Warrior_EA/Expert/ExpertSignalCustom.mqh
AnimateDread d91cabc114 refactor(MoneyIntelligent): replace streak-chasing lot sizing with fractional-Kelly criterion
Optimize() scaled lot size off account trade-history streaks with no Magic-number
filter (picked up other EAs'/manual trades) and an unconfigurable m_factor stuck at
1.0 (Factor() was never wired from an input), so a 3-trade streak could triple lot
size or send it negative. It was also entirely disconnected from what the AI model
actually knows about the current setup.

Replaced both AdjustRiskAmount()'s linear confidence-only scale and Optimize()'s
streak multiplier with one edge-based model: p from the empirically calibrated
AI/DB confidence magnitude, b from the trade's real reward:risk ratio (newly
bridged from OpenParams() via g_TradeRewardRiskRatio), quarter-Kelly applied and
clamped so risk% can only ever scale down from its configured ceiling, never above it.
2026-07-18 17:39:58 -04:00

1035 lines
48 KiB
MQL5

//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//+------------------------------------------------------------------+
#include <Expert\ExpertSignal.mqh>
#include "..\System\NewBar.mqh"
#include "..\Structures\tradeRecordStructure.mqh"
#include "..\Structures\signalInfoStructure.mqh"
#include "..\Variables\ConfidenceBridge.mqh"
//--- 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
//
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); }
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;
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; }
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)
{
g_AISignedConfidence = SignedAIConfidence();
g_DBConfidence = m_dbConfidence;
return CombinedConfidence(m_confidence_source);
}
//+------------------------------------------------------------------+
//| 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;
}
//+------------------------------------------------------------------+
//| 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");
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);
}
if(isLong)
{
sl = m_symbol.NormalizePrice(lowest_low - slMultiplier * atr);
tp = m_symbol.NormalizePrice(highest_high - TP_ATR_MULTIPLIER * atr);
}
else
{
sl = m_symbol.NormalizePrice(highest_high + slMultiplier * atr);
tp = m_symbol.NormalizePrice(lowest_low + TP_ATR_MULTIPLIER * atr);
}
// 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))
{
if(isLong)
{
sl = price - (MIN_SL_ATR_MULTIPLIER * atr);
}
else
{
sl = price + (MIN_SL_ATR_MULTIPLIER * atr);
}
}
if(fabs(tp - price) < (MIN_RR_ATR_MULTIPLIER * atr))
{
if(isLong)
{
tp = price + (MIN_RR_ATR_MULTIPLIER * atr);
}
else
{
tp = price - (MIN_RR_ATR_MULTIPLIER * atr);
}
}
// 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)
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)
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)
{
//--- 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)
{
// If the second has changed, update the last result
if(m_directionCount != 0)
{
m_directionLastResult = m_directionAggregatedResult / m_directionCount;
}
else
{
m_directionLastResult = 0.0; // Default to 0 if no results were aggregated
}
// Reset variables for the new second
m_directionAggregatedResult = 0.0;
m_directionCount = 0;
m_directionCurrentSecond = gmtTime.sec; // Update the current second
}
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++;
// Validate the aggregated result to be within the range of -100 to 100
if(m_directionLastResult < -100 || m_directionLastResult > 100)
{
m_directionLastResult = 0.0; // Set result to 0 if it's outside the range
Print("Directional result is out of range. Setting to 0.");
}
PrintVerbose("Final directional result: " + DoubleToString(m_directionLastResult));
return m_directionLastResult;
}
//+------------------------------------------------------------------+
//| 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);
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);
}
//+------------------------------------------------------------------+
//| 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;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom::DeleteOldestEntry(string tableName)
{
dbm.DeleteOldestEntry(tableName); // failure is already logged by the DB layer
}
//+------------------------------------------------------------------+
//| 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))
{
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;
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");
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)
{
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++;
}
}
// 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;
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);
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)
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;
// 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);
}
}
//+------------------------------------------------------------------+