Warrior_EA/Expert/ExpertSignalCustom.mqh

1035 行
48 KiB
MQL5
Raw パーマリンク 通常表示 履歴

2025-05-30 16:35:54 +02:00
//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//+------------------------------------------------------------------+
#include <Expert\ExpertSignal.mqh>
#include "..\System\NewBar.mqh"
#include "..\Structures\tradeRecordStructure.mqh"
#include "..\Structures\signalInfoStructure.mqh"
#include "..\Variables\ConfidenceBridge.mqh"
2025-05-30 16:35:54 +02:00
//--- Enumerations
#include "..\Enumerations\GlobalEnums.mqh"
//
#define MAX_TABLE_ROWS 1000 // row cap before the oldest entry is pruned
#define MIN_TRADES_FOR_WIN_RATE 100 // minimum sample size before a pattern's win rate is trusted
#define NO_DATA_WIN_RATE -1 // sentinel: not enough trades to compute a win rate
#define TP_ATR_MULTIPLIER 0.25
#define MIN_SL_ATR_MULTIPLIER 2.0
#define MIN_RR_ATR_MULTIPLIER 3.0
// AI-confidence scaling caps for SLTP_AI_CONFIDENCE mode - confidence is a 0..1
// scalar (see CExpertSignalAIBase::AIConfidence); SL tightens by up to 30%,
// reward target widens by up to 2x, as confidence rises from 0 to 1.
#define AI_SL_TIGHTEN_FACTOR 0.3
#define AI_RR_WIDEN_FACTOR 1.0
//
2025-05-30 16:35:54 +02:00
class CExpertSignalCustom : public CExpertSignal
{
private:
bool FetchTradeRecords(string tableName, TradeRecord &tradeRecords[]);
bool ShouldDeleteOldestEntry(TradeRecord &tradeRecords[]);
void DeleteOldestEntry(string tableName);
bool CheckForDuplicateTrade(string pattern, string direction, double entryPrice, TradeRecord &tradeRecords[]);
int FindLastTradeIndex(string pattern, string direction, TradeRecord &tradeRecords[], bool &tradeOpen);
void UpdateTradeStatusAndExit(string tableName, string pattern, string direction, double entryPrice, double exitPrice, TradeRecord &tradeRecord, TradeRecord &tradeRecords[]);
void UpdateTradeRecordInDatabase(string tableName, TradeRecord &tradeRecord);
void RegisterSignal(int year, int month, int day, int DOW, int hour, int minutes, string tableName, string pattern, string direction, double entryPrice, double exitPrice, string result);
void ProcessSignal(SignalInfo &signal);
void BufferSignal(SignalInfo &signal);
bool CheckClosePosition(bool isLong, double &price);
bool CheckOpenPosition(bool isLong, double &price, double &sl, double &tp, datetime &expiration);
void BufferNewTickSignal(string filterID, string pattern, string bias, const MqlDateTime& gmtTime, double entryPrice);
string PatternTableName(string filterID, string pattern, string direction);
string PatternName(int patternIndex) { return "Pattern_" + IntegerToString(patternIndex); }
2025-05-30 16:35:54 +02:00
SignalInfo signalBuffer[];
protected:
bool m_prohibition_signal;
bool m_useDatabase;
CiATR m_ATR; // ATR indicator
string m_id;
string m_active_pattern;
string m_active_direction;
int m_pattern_count;
double m_entry_multiplier; // Configurable multiple for ATR entry adjustment
int m_periods; // ATR periods
double m_sl_atr_multiplier; // Stop-loss distance beyond swing high/low, as a multiple of ATR
double m_min_risk_reward_ratio; // Minimum reward:risk ratio required to open a trade
bool m_use_ai_sltp; // true: scale SL/TP by EffectiveConfidence(); false: pure rule-based
int m_confidence_source; // CONFIDENCE_SOURCE underlying int (0=AI, 1=DB, 2=Blended)
bool m_use_ai_exit; // true: close early when confidence reverses against the position
double m_ai_exit_threshold; // 0..1 min. confidence required to trigger an early AI exit
double m_dbConfidence; // last average normalized DB win-rate across active filters
//--- Direction()'s per-second aggregation state. MUST be per-instance, not function-local statics -
//--- Direction() is inherited as-is (not overridden) by every CExpertSignalCustom subclass that
//--- doesn't provide its own (the root "signal" object AND CExpertSignalAIBase, so PAI/CONV/LSTM),
//--- meaning they'd all share one compiled function body. Function-local statics there would be a
//--- single instance shared across the root signal and every AI filter, each stomping on the
//--- others' in-progress per-second average instead of keeping their own.
int m_directionCurrentSecond;
double m_directionAggregatedResult;
int m_directionCount;
double m_directionLastResult;
2025-05-30 16:35:54 +02:00
public:
CExpertSignalCustom(void);
~CExpertSignalCustom(void);
virtual bool AddFilter(CExpertSignal *filter);
virtual bool CheckOpenLong(double &price, double &sl, double &tp, datetime &expiration) override;
virtual bool CheckOpenShort(double &price, double &sl, double &tp, datetime &expiration) override;
virtual bool CheckCloseLong(double &price) override;
virtual bool CheckCloseShort(double &price) override;
bool OpenParams(bool isLong, double &price, double &sl, double &tp, datetime &expiration); // Added for generalized parameter calculation
virtual bool OpenLongParams(double &price, double &sl, double &tp, datetime &expiration) override;
virtual bool OpenShortParams(double &price, double &sl, double &tp, datetime &expiration) override;
virtual bool ValidationSettings(void) override;
virtual bool InitIndicators(CIndicators *indicators) override;
void Entry_Multiplier(double entry_multiplier) { m_entry_multiplier = entry_multiplier; }
void Periods(int periods) { m_periods = periods; }
void SLAtrMultiplier(double value) { m_sl_atr_multiplier = value; }
void MinRiskRewardRatio(double value) { m_min_risk_reward_ratio = value; }
void UseAISLTP(bool value) { m_use_ai_sltp = value; }
void ConfidenceSource(int value) { m_confidence_source = value; }
void UseAIExit(bool value) { m_use_ai_exit = value; }
void AIExitThreshold(double value) { m_ai_exit_threshold = value; }
// 0.0 = no AI confidence available (pure rule-based); overridden in
// CExpertSignalAIBase to return the live signal's confidence in [0,1].
virtual double AIConfidence(void) { return 0.0; }
// Signed version of AIConfidence: sign gives direction (+ buy, - sell), used for
// AI-driven early exit. 0.0 = no AI filter (base rule-based class never exits early).
virtual double SignedAIConfidence(void) { return 0.0; }
// Combines AIConfidence()/DBConfidence() per m_confidence_source into a single 0..1
// magnitude, used to scale SL/TP and (Intelligent MM) lot size.
double EffectiveConfidence(void);
double DBConfidence(void) { return m_dbConfidence; }
2025-05-30 16:35:54 +02:00
virtual void ApplyPatternWeight(int patternNumber, int weight) {};
void ID(string id) { m_id = id; }
virtual string GetFilterID(void) { return m_id; };
virtual string GetActivePattern(void);
virtual string GetActiveDirection(void);
virtual int GetPatternCount(void) { return m_pattern_count; };
virtual double Direction(void) override;
bool UpdateSignalsWeights(void);
int CalculatePatternWinRate(string pattern, TradeRecord &tr[]);
int NormalizeWinRate(double winRate);
void ProcessBufferedSignals(void);
bool InRange(double value, double min, double max); // Helper function for range checking
void UseDatabase(bool value) { m_useDatabase = value; };
//--- event handler
virtual void OnTickHandler(void);
virtual void OnChartEventHandler(const int id,
const long &lparam,
const double &dparam,
const string &sparam);
};
//+------------------------------------------------------------------+
//| Constructor |
//+------------------------------------------------------------------+
CExpertSignalCustom::CExpertSignalCustom(void) :
m_id("NULL"),
m_active_pattern("NULL"),
m_active_direction("NULL"),
m_pattern_count(0),
m_entry_multiplier(0),
m_prohibition_signal(false),
m_periods(14),
m_useDatabase(false),
m_sl_atr_multiplier(3.0),
m_min_risk_reward_ratio(2.0),
m_use_ai_sltp(false),
m_confidence_source(0),
m_use_ai_exit(false),
m_ai_exit_threshold(0.7),
m_dbConfidence(0.0),
m_directionCurrentSecond(-1),
m_directionAggregatedResult(0.0),
m_directionCount(0),
m_directionLastResult(0.0)
{
}
//+------------------------------------------------------------------+
//| Combine AI/DB confidence per the configured Confidence_Source |
//+------------------------------------------------------------------+
double CExpertSignalCustom::EffectiveConfidence(void)
2025-05-30 16:35:54 +02:00
{
g_AISignedConfidence = SignedAIConfidence();
g_DBConfidence = m_dbConfidence;
return CombinedConfidence(m_confidence_source);
2025-05-30 16:35:54 +02:00
}
//+------------------------------------------------------------------+
//| Destructor |
//+------------------------------------------------------------------+
CExpertSignalCustom::~CExpertSignalCustom(void)
{
ArrayFree(signalBuffer);
}
//+------------------------------------------------------------------+
//| Single source of truth for the per-pattern/direction table name |
//+------------------------------------------------------------------+
string CExpertSignalCustom::PatternTableName(string filterID, string pattern, string direction)
{
return filterID + "_" + pattern + "_" + direction;
}
//+------------------------------------------------------------------+
2025-05-30 16:35:54 +02:00
//| Helper function to check value ranges |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::InRange(double value, double min, double max)
{
return value >= min && value <= max;
}
//+------------------------------------------------------------------+
//| Validation settings protected data |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::ValidationSettings(void)
{
if(!CExpertSignal::ValidationSettings())
return false;
// Simplified checks using the InRange helper
if(!InRange(m_periods, 0, 200))
{
printf(__FUNCTION__ ": ATR Periods must be 0-200");
return false;
}
if(!InRange(StartIndex(), 0, 200))
{
printf(__FUNCTION__ ": ATR shift must be 0-200");
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Create indicators |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::InitIndicators(CIndicators *indicators)
{
//--- check pointer
if(indicators == NULL)
return(false);
//---
CExpertSignal *filter;
int total = m_filters.Total();
//--- gather information about using of timeseries
for(int i = 0; i < total; i++)
{
filter = m_filters.At(i);
m_used_series |= filter.UsedSeries();
}
//--- create required timeseries
if(!CExpertBase::InitIndicators(indicators))
return(false);
//--- initialization of indicators and timeseries in the additional filters
for(int i = 0; i < total; i++)
{
filter = m_filters.At(i);
filter.SetPriceSeries(m_open, m_high, m_low, m_close);
filter.SetOtherSeries(m_spread, m_time, m_tick_volume, m_real_volume);
if(!filter.InitIndicators(indicators))
return(false);
}
if(!indicators.Add(GetPointer(m_ATR)) || !m_ATR.Create(m_symbol.Name(), m_period, m_periods) || !CExpertSignal::InitIndicators(indicators))
{
printf(__FUNCTION__ ": error initializing indicators");
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Setting an additional filter |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::AddFilter(CExpertSignal *filter)
{
if(filter == NULL)
return false;
if(!filter.Init(m_symbol, m_period, m_adjusted_point))
return false;
if(!m_filters.Add(filter))
return false;
filter.EveryTick(m_every_tick);
filter.Magic(m_magic);
CExpertSignalCustom *customFilter = dynamic_cast<CExpertSignalCustom*>(filter);
if(customFilter != NULL)
{
string filterID = customFilter.GetFilterID();
if(filterID != "NULL" && m_useDatabase)
{
int patternCount = customFilter.GetPatternCount();
for(int i = 0; i < patternCount; i++)
{
string tableNameBuy = PatternTableName(filterID, PatternName(i), "Buy");
string tableNameSell = PatternTableName(filterID, PatternName(i), "Sell");
2025-05-30 16:35:54 +02:00
dbm.CreateTable(tableNameBuy, tableschema); // Create table for Buy direction
dbm.CreateTable(tableNameSell, tableschema); // Create table for Sell direction
}
}
}
return true;
}
//+------------------------------------------------------------------+
//| Wrapper functions for buying and selling parameters |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::OpenParams(bool isLong, double &price, double &sl, double &tp, datetime &expiration)
{
int idx = StartIndex();
double atr = m_ATR.Main(idx);
if(atr <= 0.0)
return false; // ATR must be positive
if(!m_symbol.Name(_Symbol))
return false; // Symbol information must be accessible
int lookback_period = m_periods;
double base_price = (m_base_price == 0.0) ? (isLong ? m_symbol.Ask() : m_symbol.Bid()) : m_base_price;
price = base_price; // Entry price
int lowest_index = iLowest(_Symbol, PERIOD_CURRENT, MODE_LOW, lookback_period, idx);
int highest_index = iHighest(_Symbol, PERIOD_CURRENT, MODE_HIGH, lookback_period, idx);
double lowest_low = Low(lowest_index);
double highest_high = High(highest_index);
double slMultiplier = m_sl_atr_multiplier;
double minRR = m_min_risk_reward_ratio;
// Refresh the confidence bridge every time regardless of m_use_ai_sltp, so that
// Use_AI_Lot_Sizing (Money\MoneyIntelligent.mqh) sees a fresh value even when the
// SL/TP sizing itself is left on rule-based.
double confidence = EffectiveConfidence();
if(m_use_ai_sltp)
{
slMultiplier *= (1.0 - AI_SL_TIGHTEN_FACTOR * confidence);
minRR *= (1.0 + AI_RR_WIDEN_FACTOR * confidence);
}
2025-05-30 16:35:54 +02:00
if(isLong)
{
sl = m_symbol.NormalizePrice(lowest_low - slMultiplier * atr);
tp = m_symbol.NormalizePrice(highest_high - TP_ATR_MULTIPLIER * atr);
2025-05-30 16:35:54 +02:00
}
else
{
sl = m_symbol.NormalizePrice(highest_high + slMultiplier * atr);
tp = m_symbol.NormalizePrice(lowest_low + TP_ATR_MULTIPLIER * atr);
2025-05-30 16:35:54 +02:00
}
// Ensure SL and TP are at least MIN_SL/RR_ATR_MULTIPLIER x ATR from entry price
if(fabs(price - sl) < (MIN_SL_ATR_MULTIPLIER * atr))
2025-05-30 16:35:54 +02:00
{
if(isLong)
{
sl = price - (MIN_SL_ATR_MULTIPLIER * atr);
2025-05-30 16:35:54 +02:00
}
else
{
sl = price + (MIN_SL_ATR_MULTIPLIER * atr);
2025-05-30 16:35:54 +02:00
}
}
if(fabs(tp - price) < (MIN_RR_ATR_MULTIPLIER * atr))
2025-05-30 16:35:54 +02:00
{
if(isLong)
{
tp = price + (MIN_RR_ATR_MULTIPLIER * atr);
2025-05-30 16:35:54 +02:00
}
else
{
tp = price - (MIN_RR_ATR_MULTIPLIER * atr);
2025-05-30 16:35:54 +02:00
}
}
// Check if the risk-to-reward ratio is at least 1:minRR (Min_Risk_Reward_Ratio input,
// widened further by AI confidence when SLTP_Source == SLTP_AI_CONFIDENCE)
2025-05-30 16:35:54 +02:00
double risk = fabs(price - sl);
double reward = fabs(tp - price);
// Bridged to Money\MoneyIntelligent.mqh's Kelly-criterion sizing the same way as
// EffectiveConfidence() above - refreshed regardless of outcome below, since a rejected
// setup here never reaches Money.CheckOpenLong/Short() this tick anyway.
g_TradeRewardRiskRatio = (risk > 0.0) ? reward / risk : 0.0;
if(reward < minRR * risk)
2025-05-30 16:35:54 +02:00
return false;
// Adjust expiration time
expiration += m_expiration * PeriodSeconds(m_period);
return true;
}
//+------------------------------------------------------------------+
//| Detecting the levels for buying |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::OpenLongParams(double &price, double &sl, double &tp, datetime &expiration)
{
return OpenParams(true, price, sl, tp, expiration);
}
//+------------------------------------------------------------------+
//| Detecting the levels for selling |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::OpenShortParams(double &price, double &sl, double &tp, datetime &expiration)
{
return OpenParams(false, price, sl, tp, expiration);
}
//+------------------------------------------------------------------+
//| Common function for closing positions |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::CheckClosePosition(bool isLong, double &price)
{
bool result = false;
//--- check of exceeding the threshold value, adjusted for long/short
double directionMultiplier = isLong ? -1 : 1;
// Allowing position closing without checking the prohibition signal.
if(directionMultiplier * m_direction >= m_threshold_close)
result = true;
// AI-driven early exit: close regardless of the rule-based threshold above if the AI
// signal has flipped against the open position with at least m_ai_exit_threshold
// confidence. SignedAIConfidence() is 0.0 for non-AI signal filters, so this is a
// no-op unless both Use_AI_Exit is set and the active filter is AI-driven.
if(!result && m_use_ai_exit)
{
double signed_conf = SignedAIConfidence();
bool reversedAgainstLong = isLong && signed_conf < 0.0 && MathAbs(signed_conf) >= m_ai_exit_threshold;
bool reversedAgainstShort = !isLong && signed_conf > 0.0 && MathAbs(signed_conf) >= m_ai_exit_threshold;
if(reversedAgainstLong || reversedAgainstShort)
result = true;
}
if(result)
{
2025-05-30 16:35:54 +02:00
//--- try to get the level of closing, differentiating based on isLong
if(!(isLong ? CloseLongParams(price) : CloseShortParams(price)))
result = false;
}
//--- zeroize the base price
m_base_price = 0.0;
//--- return the result
return result;
}
//+------------------------------------------------------------------+
//| Generating a signal for closing of a long position |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::CheckCloseLong(double &price)
{
return CheckClosePosition(true, price);
}
//+------------------------------------------------------------------+
//| Generating a signal for closing a short position |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::CheckCloseShort(double &price)
{
return CheckClosePosition(false, price);
}
//+------------------------------------------------------------------+
//| Common function for opening positions |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::CheckOpenPosition(bool isLong, double &price, double &sl, double &tp, datetime &expiration)
{
bool result = false;
//--- the "prohibition" signal
if(m_prohibition_signal == true)
return false;
//--- check of exceeding the threshold value, adjusted for long/short
double directionMultiplier = isLong ? 1 : -1;
if(directionMultiplier * m_direction >= m_threshold_open)
{
//--- there's a signal
result = true;
//--- try to get the levels of opening, differentiating based on isLong
if(!(isLong ? OpenLongParams(price, sl, tp, expiration) : OpenShortParams(price, sl, tp, expiration)))
result = false;
}
//--- zeroize the base price
m_base_price = 0.0;
//--- return the result
return result;
}
//+------------------------------------------------------------------+
//| Generating a buy signal |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::CheckOpenLong(double &price, double &sl, double &tp, datetime &expiration)
{
// Check if the trading strategy allows opening long positions
if(tradingdirection == LONG_ONLY || tradingdirection == BOTH)
{
return CheckOpenPosition(true, price, sl, tp, expiration);
}
// If the strategy is SHORT_ONLY, prevent opening a long position
return false;
}
//+------------------------------------------------------------------+
//| Generating a sell signal |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::CheckOpenShort(double &price, double &sl, double &tp, datetime &expiration)
{
// Check if the trading strategy allows opening short positions
if(tradingdirection == SHORT_ONLY || tradingdirection == BOTH)
{
return CheckOpenPosition(false, price, sl, tp, expiration);
}
// If the strategy is LONG_ONLY, prevent opening a short position
return false;
}
//+------------------------------------------------------------------+
//| Return the currently active pattern |
//+------------------------------------------------------------------+
string CExpertSignalCustom::GetActivePattern(void)
{
string ret = m_active_pattern;
m_active_pattern = "NULL";
return ret;
}
//+------------------------------------------------------------------+
//| Return the currently active direction |
//+------------------------------------------------------------------+
string CExpertSignalCustom::GetActiveDirection(void)
{
string ret = m_active_direction;
m_active_direction = "NULL";
return ret;
}
//+------------------------------------------------------------------+
//| Detecting the "weighted" direction |
//+------------------------------------------------------------------+
double CExpertSignalCustom::Direction(void)
{
MqlDateTime gmtTime;
TimeGMT(gmtTime);
// Check if the current second has changed
if(gmtTime.sec != m_directionCurrentSecond)
2025-05-30 16:35:54 +02:00
{
// If the second has changed, update the last result
if(m_directionCount != 0)
2025-05-30 16:35:54 +02:00
{
m_directionLastResult = m_directionAggregatedResult / m_directionCount;
2025-05-30 16:35:54 +02:00
}
else
{
m_directionLastResult = 0.0; // Default to 0 if no results were aggregated
2025-05-30 16:35:54 +02:00
}
// Reset variables for the new second
m_directionAggregatedResult = 0.0;
m_directionCount = 0;
m_directionCurrentSecond = gmtTime.sec; // Update the current second
2025-05-30 16:35:54 +02:00
}
m_prohibition_signal = false;
double result = m_weight * (LongCondition() - ShortCondition());
int number = (result == 0.0) ? 0 : 1;
int total = m_filters.Total();
PrintVerbose("Starting direction calculation with total filters: " + IntegerToString(total));
for(int i = 0; i < total; i++)
{
long mask = ((long)1) << i;
if((m_ignore & mask) != 0)
continue;
CExpertSignalCustom *filter = m_filters.At(i);
if(filter == NULL)
{
Print("Error: Filter at index " + IntegerToString(i) + " is NULL");
continue;
}
double price = 0.0, sl = 0.0, tp = 0.0;
datetime expiration = 0;
string bias = filter.GetActiveDirection();
string filterID = filter.GetFilterID();
string pattern = filter.GetActivePattern();
if(filterID != "NULL" && bias != "NULL" && pattern != "NULL" && m_useDatabase)
{
PrintVerbose("Processing filter: " + filterID + ", Bias: " + bias + ", Pattern: " + pattern);
double newPrice = 0;
bool signalBuffered = false; // Flag to track if signal was buffered
if(bias == "Buy")
{
if(OpenLongParams(price, sl, tp, expiration))
{
newPrice = m_symbol.Ask(); // Adjust price to current ask price
signalBuffered = true; // Set flag to true as signal will be buffered
}
}
else
if(bias == "Sell")
{
if(OpenShortParams(price, sl, tp, expiration))
{
newPrice = m_symbol.Bid(); // Adjust price to current bid price
signalBuffered = true; // Set flag to true as signal will be buffered
}
}
if(signalBuffered)
{
BufferNewTickSignal(filterID, pattern, bias, gmtTime, newPrice);
}
}
double direction = filter.Direction();
if(direction == EMPTY_VALUE)
{
m_prohibition_signal = true;
continue;
}
if(direction != EMPTY_VALUE)
{
// Validate the result to be within the range of -100 to 100
if(direction < -100 || direction > 100)
{
PrintVerbose("A filter's direction is invalid. Skipping tick.");
direction = EMPTY_VALUE;
result = 0;
number = 0;
break;
}
if(direction != 0)
{
number++; // Only increment `number` if `direction` is not 0 or EMPTY_VALUE
result += ((m_invert & mask) != 0) ? -direction : direction;
}
}
}
// Aggregate the result and count
m_directionAggregatedResult += result;
m_directionCount++;
2025-05-30 16:35:54 +02:00
// Validate the aggregated result to be within the range of -100 to 100
if(m_directionLastResult < -100 || m_directionLastResult > 100)
2025-05-30 16:35:54 +02:00
{
m_directionLastResult = 0.0; // Set result to 0 if it's outside the range
2025-05-30 16:35:54 +02:00
Print("Directional result is out of range. Setting to 0.");
}
PrintVerbose("Final directional result: " + DoubleToString(m_directionLastResult));
return m_directionLastResult;
2025-05-30 16:35:54 +02:00
}
//+------------------------------------------------------------------+
//| handles the new bar signal buffering |
//+------------------------------------------------------------------+
void CExpertSignalCustom::BufferNewTickSignal(string filterID, string pattern, string bias, const MqlDateTime& gmtTime, double entryPrice)
{
if(filterID == "NULL" || pattern == "NULL" || bias == "NULL")
{
Print("Error buffering new tick signal: Invalid filter parameters - filterID: '" + filterID +
"', pattern: '" + pattern + "', bias: '" + bias + "'.");
return;
}
string tableName = PatternTableName(filterID, pattern, bias);
2025-05-30 16:35:54 +02:00
SignalInfo signal = {gmtTime.year, gmtTime.mon, gmtTime.day, gmtTime.day_of_week, gmtTime.hour, gmtTime.min, tableName, pattern, bias, entryPrice};
BufferSignal(signal);
PrintVerbose("New tick signal buffered: " + tableName + ", Pattern: " + pattern + ", Bias: " + bias + ", Entry Price: " + DoubleToString(entryPrice));
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom::BufferSignal(SignalInfo &signal)
{
// Check for duplicate signals in the buffer
for(int i = 0; i < ArraySize(signalBuffer); i++)
{
if(signalBuffer[i].tableName == signal.tableName &&
signalBuffer[i].pattern == signal.pattern &&
signalBuffer[i].direction == signal.direction)
{
PrintVerbose("Duplicate signal detected, not adding to buffer: " + signal.tableName + ", Pattern: " + signal.pattern + ", Direction: " + signal.direction);
return; // Skip buffering if a duplicate is found
}
}
// Resize the buffer and add the new signal
ArrayResize(signalBuffer, ArraySize(signalBuffer) + 1);
signalBuffer[ArraySize(signalBuffer) - 1] = signal;
PrintVerbose("Signal buffered for: " + signal.tableName + ", Pattern: " + signal.pattern + ", Direction: " + signal.direction);
}
//+------------------------------------------------------------------+
//| Process the signal and update trades |
//+------------------------------------------------------------------+
void CExpertSignalCustom::ProcessSignal(SignalInfo &signal)
{
string currentTableName = signal.tableName;
string oppositeTableName = currentTableName; // Start with a copy of the current table name
PrintVerbose("Processing signal for table: " + currentTableName);
// Swap the direction in the table name to get the opposite table name
if(signal.direction == "Buy")
{
StringReplace(oppositeTableName, "Buy", "Sell");
PrintVerbose("Swapped to opposite table: " + oppositeTableName + " from Buy to Sell");
}
else
{
StringReplace(oppositeTableName, "Sell", "Buy");
PrintVerbose("Swapped to opposite table: " + oppositeTableName + " from Sell to Buy");
}
// Fetch trade records for both directions
TradeRecord tradeRecordsCurrent[], tradeRecordsOpposite[];
if(!FetchTradeRecords(currentTableName, tradeRecordsCurrent))
{
Print("Failed to fetch current direction trades from: " + currentTableName);
return; // Fail to fetch current direction trades
}
if(!FetchTradeRecords(oppositeTableName, tradeRecordsOpposite))
{
Print("Failed to fetch opposite direction trades from: " + oppositeTableName);
return; // Fail to fetch opposite direction trades
}
if(ShouldDeleteOldestEntry(tradeRecordsCurrent))
DeleteOldestEntry(currentTableName);
if(ShouldDeleteOldestEntry(tradeRecordsOpposite))
DeleteOldestEntry(oppositeTableName);
// Process trades in the opposite direction to close them
bool isTradeOpen = false;
for(int i = 0; i < ArraySize(tradeRecordsOpposite); i++)
{
if(tradeRecordsOpposite[i].pattern == signal.pattern && tradeRecordsOpposite[i].result == "NA")
{
// Close the opposite trade
tradeRecordsOpposite[i].exitPrice = signal.entryPrice;
double profitLoss = (tradeRecordsOpposite[i].direction == "Buy") ?
(signal.entryPrice - tradeRecordsOpposite[i].entryPrice) :
(tradeRecordsOpposite[i].entryPrice - signal.entryPrice);
tradeRecordsOpposite[i].result = profitLoss >= 0 ? "Profit" : "Loss";
UpdateTradeRecordInDatabase(oppositeTableName, tradeRecordsOpposite[i]);
PrintVerbose("Closed opposite trade: " + oppositeTableName + ", Profit/Loss: " + DoubleToString(profitLoss));
isTradeOpen = true; // Signal that a trade was handled
break; // Since it's a stop and reverse, handle only one trade at a time
}
}
// Check for open trades or duplicate entries in the current direction
for(int i = 0; i < ArraySize(tradeRecordsCurrent); i++)
{
// Check for exact duplicates first
if(tradeRecordsCurrent[i].pattern == signal.pattern &&
tradeRecordsCurrent[i].year == signal.year &&
tradeRecordsCurrent[i].month == signal.month &&
tradeRecordsCurrent[i].day == signal.day &&
tradeRecordsCurrent[i].hour == signal.hour &&
tradeRecordsCurrent[i].minutes == signal.minutes)
{
PrintVerbose("Duplicate trade found, not registering new trade. Table: " + currentTableName);
return; // Duplicate trade found, exit processing
}
// Check for outdated or same time trades
if((tradeRecordsCurrent[i].year > signal.year) ||
(tradeRecordsCurrent[i].year == signal.year && tradeRecordsCurrent[i].month > signal.month) ||
(tradeRecordsCurrent[i].year == signal.year && tradeRecordsCurrent[i].month == signal.month && tradeRecordsCurrent[i].day > signal.day) ||
(tradeRecordsCurrent[i].year == signal.year && tradeRecordsCurrent[i].month == signal.month && tradeRecordsCurrent[i].day == signal.day && tradeRecordsCurrent[i].hour > signal.hour) ||
(tradeRecordsCurrent[i].year == signal.year && tradeRecordsCurrent[i].month == signal.month && tradeRecordsCurrent[i].day == signal.day && tradeRecordsCurrent[i].hour == signal.hour && tradeRecordsCurrent[i].minutes >= signal.minutes))
{
PrintVerbose("Outdated or same time trade found, not registering new trade. Table: " + currentTableName);
return; // Outdated or same time trade found, exit processing
}
// Check if there's an open trade with the same pattern
if(tradeRecordsCurrent[i].result == "NA" && tradeRecordsCurrent[i].pattern == signal.pattern)
{
PrintVerbose("Open trade found, not registering new trade. Table: " + currentTableName + ", Pattern: " + signal.pattern);
return; // Open trade found, exit processing
}
}
// Register a new trade if no duplicates, outdated, or open trades are found
if(!isTradeOpen)
{
RegisterSignal(signal.year, signal.month, signal.day, signal.DOW, signal.hour, signal.minutes,
currentTableName, signal.pattern, signal.direction, signal.entryPrice, 0.0, "NA");
PrintVerbose("Registered new trade in table: " + currentTableName + ", Pattern: " + signal.pattern + ", Direction: " + signal.direction);
}
}
//+------------------------------------------------------------------+
//| Helper function to compare two datetime values |
//+------------------------------------------------------------------+
bool IsEarlier(const SignalInfo& a, const SignalInfo& b)
{
datetime dtA = MakeDateTime(a);
datetime dtB = MakeDateTime(b);
return dtA < dtB;
}
//+------------------------------------------------------------------+
//| Selection sort for sorting SignalInfo array by datetime |
//+------------------------------------------------------------------+
void SelectionSort(SignalInfo &signals[], int size)
{
for(int i = 0; i < size - 1; i++)
{
int min_idx = i;
for(int j = i + 1; j < size; j++)
{
if(IsEarlier(signals[j], signals[min_idx]))
{
min_idx = j;
}
}
if(min_idx != i)
{
// Swapping the elements
SignalInfo temp = signals[i];
signals[i] = signals[min_idx];
signals[min_idx] = temp;
}
}
}
//+------------------------------------------------------------------+
//| Helper function to create a sortable datetime value |
//+------------------------------------------------------------------+
datetime MakeDateTime(const SignalInfo &signal)
{
MqlDateTime t;
t.year = signal.year;
t.mon = signal.month;
t.day = signal.day;
t.hour = signal.hour;
t.min = signal.minutes;
t.sec = 0;
return StructToTime(t);
2025-05-30 16:35:54 +02:00
}
//+------------------------------------------------------------------+
//| Process the signal and update trades |
//+------------------------------------------------------------------+
void CExpertSignalCustom::ProcessBufferedSignals()
{
// Sort the signals array by datetime before processing
SelectionSort(signalBuffer, ArraySize(signalBuffer));
if(!dbm.OpenDatabase())
{
Print("Failed to open database.");
return;
}
if(!dbm.BeginTransaction())
{
Print("Failed to begin database transaction.");
return;
}
for(int i = 0; i < ArraySize(signalBuffer); i++)
{
PrintVerbose("Processing signal " + IntegerToString(i + 1) + " of " + IntegerToString(ArraySize(signalBuffer)));
ProcessSignal(signalBuffer[i]);
}
if(!dbm.CommitTransaction())
{
Print("Failed to commit the transaction to the database.");
return;
}
ArrayResize(signalBuffer, 0);
PrintVerbose("Signal buffer cleared after processing.");
if(!IsBacktesting)
{
dbm.CloseDatabase();
PrintVerbose("Database closed successfully.");
}
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::FetchTradeRecords(string tableName, TradeRecord &tradeRecords[])
{
TradeRecord tradeRecordStruct;
if(!dbm.FetchTradeRecords(tableName, tradeRecordStruct, tradeRecords))
{
Print(__FUNCTION__ + " Failed to fetch trade records from " + tableName);
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::ShouldDeleteOldestEntry(TradeRecord &tradeRecords[])
{
return ArraySize(tradeRecords) >= MAX_TABLE_ROWS;
2025-05-30 16:35:54 +02:00
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom::DeleteOldestEntry(string tableName)
{
dbm.DeleteOldestEntry(tableName); // failure is already logged by the DB layer
2025-05-30 16:35:54 +02:00
}
//+------------------------------------------------------------------+
//| Register a signal in the database |
//+------------------------------------------------------------------+
void CExpertSignalCustom::RegisterSignal(int year, int month, int day, int DOW, int hour, int minutes, string tableName, string pattern, string direction, double entryPrice, double exitPrice, string result)
{
string Columns[] = {"year", "month", "day", "dayOfWeek", "hour", "minutes", "pattern", "direction", "entryPrice", "exitPrice", "result"};
string valArr[] = {IntegerToString(year), IntegerToString(month), IntegerToString(day), IntegerToString(DOW), IntegerToString(hour), IntegerToString(minutes), pattern, direction, DoubleToString(entryPrice, Digits()), DoubleToString(exitPrice, Digits()), result};
if(dbm.InsertTradeRecord(tableName, Columns, valArr))
{
PrintVerbose("Successfully registered signal in table: " + tableName);
}
else
{
Print("Failed to register signal in table: " + tableName);
}
}
//+------------------------------------------------------------------+
//| Update a trade record in the database |
//+------------------------------------------------------------------+
void CExpertSignalCustom::UpdateTradeRecordInDatabase(string tableName, TradeRecord &tradeRecord)
{
string columns[] = { "exitPrice", "result" };
string values[] = { DoubleToString(tradeRecord.exitPrice, Digits()), tradeRecord.result };
if(dbm.UpdateTradeRecord(tableName, columns, values, tradeRecord.pattern, tradeRecord.direction))
2025-05-30 16:35:54 +02:00
{
PrintVerbose("Successfully updated trade record in table: " + tableName);
}
else
{
Print("Failed to update trade record in table: " + tableName + " for pattern " + tradeRecord.pattern + " and direction " + tradeRecord.direction);
}
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::UpdateSignalsWeights(void)
{
if(!dbm.BeginTransaction())
return(false);
TradeRecord tradeRecordStruct;
int total = m_filters.Total();
double sumModuleWeight = 0.0;
int weightedFilterCount = 0;
2025-05-30 16:35:54 +02:00
for(int i = 0; i < total; i++)
{
CExpertSignalCustom *filter = m_filters.At(i);
//--- check pointer
if(filter == NULL)
continue;
string filterID = filter.GetFilterID();
if(filterID == "NULL")
continue;
int patternCount = filter.GetPatternCount();
if(patternCount <= 0 || patternCount == NULL)
continue;
int totalWinRate = 0;
int validPatternCount = 0;
for(int j = 0; j < patternCount; j++)
{
// Fetch trade records for the specified table
string pattern = PatternName(j);
string tableNameBuy = PatternTableName(filterID, pattern, "Buy");
string tableNameSell = PatternTableName(filterID, pattern, "Sell");
2025-05-30 16:35:54 +02:00
TradeRecord tradeRecordsBuy[], tradeRecordsSell[];
// Fetch Buy side trade records
if(!dbm.FetchTradeRecords(tableNameBuy, tradeRecordStruct, tradeRecordsBuy))
{
Print(__FUNCTION__ + " Failed to fetch trade records from " + tableNameBuy);
continue;
}
// Fetch Sell side trade records
if(!dbm.FetchTradeRecords(tableNameSell, tradeRecordStruct, tradeRecordsSell))
{
Print(__FUNCTION__ + " Failed to fetch trade records from " + tableNameSell);
continue;
}
int winRateBuy = CalculatePatternWinRate(pattern, tradeRecordsBuy);
int winRateSell = CalculatePatternWinRate(pattern, tradeRecordsSell);
// Skip sides with insufficient samples instead of averaging in the sentinel
if(winRateBuy == NO_DATA_WIN_RATE && winRateSell == NO_DATA_WIN_RATE)
continue;
int combinedWinRate = (winRateBuy == NO_DATA_WIN_RATE) ? winRateSell :
(winRateSell == NO_DATA_WIN_RATE) ? winRateBuy :
(winRateBuy + winRateSell) / 2;
if(combinedWinRate >= 0 && combinedWinRate <= 100)
2025-05-30 16:35:54 +02:00
{
filter.ApplyPatternWeight(j, combinedWinRate);
totalWinRate += combinedWinRate;
validPatternCount++;
PrintVerbose("Applied " + filterID + " " + pattern + " Weight " + IntegerToString(combinedWinRate));
}
}
// Calculate the average win rate for valid patterns
double averageWinRate = validPatternCount > 0 ? (totalWinRate) / validPatternCount : 0.0;
// Normalize the average win rate to the range 0 to 1
double normalizedWinRate = averageWinRate / 100.0;
// Round the normalized win rate to the nearest 0.05
normalizedWinRate = MathRound(normalizedWinRate * 10) / 10.0;
// Ensure the rounded value is within 0 to 1
normalizedWinRate = MathMax(0, MathMin(normalizedWinRate, 1));
// Apply the main weight based on the normalized and rounded win rate
double moduleWeight = normalizedWinRate;
if(moduleWeight > 0 && moduleWeight <= 1)
{
filter.Weight(moduleWeight);
PrintVerbose("Applied " + filterID + " Main Weight " + DoubleToString(moduleWeight, 2));
}
if(validPatternCount > 0)
{
sumModuleWeight += normalizedWinRate;
weightedFilterCount++;
}
2025-05-30 16:35:54 +02:00
}
// Track the overall DB win-rate confidence across all filters, so it can be
// combined with (or used instead of) AI confidence via Confidence_Source.
m_dbConfidence = weightedFilterCount > 0 ? sumModuleWeight / weightedFilterCount : 0.0;
2025-05-30 16:35:54 +02:00
if(dbm.CommitTransaction())
return true;
else
return(false);
}
//+------------------------------------------------------------------+
//| Calculate the time based win rate for specified pattern |
//+------------------------------------------------------------------+
int CExpertSignalCustom::CalculatePatternWinRate(string pattern, TradeRecord & tr[])
{
int totalTrades = 0;
int profitableTrades = 0;
MqlDateTime gmtTime;
TimeGMT(gmtTime);
if(IsBacktesting)
{
datetime nowGmt = StructToTime(gmtTime);
2025-05-30 16:35:54 +02:00
for(int i = ArraySize(tr) - 1; i >= 0; i--)
{
MqlDateTime recordTime;
recordTime.year = tr[i].year;
recordTime.mon = tr[i].month;
recordTime.day = tr[i].day;
recordTime.hour = tr[i].hour;
recordTime.min = tr[i].minutes;
recordTime.sec = 0;
if(StructToTime(recordTime) >= nowGmt)
2025-05-30 16:35:54 +02:00
ArrayResize(tr, ArraySize(tr) - 1);
else
break;
}
}
// Loop through trade records
for(int i = 0; i < ArraySize(tr); i++)
{
if(tr[i].pattern == pattern && tr[i].result != "NA")
{
if(tr[i].hour == gmtTime.hour && tr[i].day == gmtTime.day && tr[i].day_of_week == gmtTime.day_of_week && tr[i].month == gmtTime.mon)
{
totalTrades++; // Increment total trades
if(tr[i].result == "Profit")
profitableTrades++;
}
}
}
// Check if total trades meet the minimum requirement
if(totalTrades < MIN_TRADES_FOR_WIN_RATE)
return NO_DATA_WIN_RATE;
2025-05-30 16:35:54 +02:00
// Calculate win rate based on the selected trading style
double winRate = 0.0;
winRate = (totalTrades > 2) ? (double)profitableTrades / totalTrades * 100.0 : 0.0;
// Normalize and return win rate
return NormalizeWinRate(winRate);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
int CExpertSignalCustom::NormalizeWinRate(double winRate)
{
return (int)MathRound(winRate / 10) * 10; // Round to the nearest 10
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom::OnTickHandler(void)
{
int total = m_filters.Total();
for(int i = 0; i < total; i++)
{
CExpertSignalCustom *filter = m_filters.At(i);
//--- check pointer
if(filter == NULL)
continue;
string filterID = filter.GetFilterID();
if(filterID == "NULL")
continue;
filter.OnTickHandler();
}
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom::OnChartEventHandler(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
int total = m_filters.Total();
for(int i = 0; i < total; i++)
{
CExpertSignalCustom *filter = m_filters.At(i);
//--- check pointer
if(filter == NULL)
continue;
string filterID = filter.GetFilterID();
if(filterID == "NULL")
continue;
filter.OnChartEventHandler(id, lparam, dparam, sparam);
}
}
//+------------------------------------------------------------------+