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"
2026-07-13 03:23:39 -04:00
# include "..\Variables\ConfidenceBridge.mqh"
2026-07-26 23:08:32 -04:00
# include "..\System\TradeChecks.mqh"
2025-05-30 16:35:54 +02:00
//--- Enumerations
# include "..\Enumerations\GlobalEnums.mqh"
//
2026-07-13 03:23:39 -04:00
# 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
2026-07-22 13:33:56 -04:00
# define MIN_SL_ATR_MULTIPLIER 2.0 / / hard floor on SL distance from entry ( broker stop - level / sanity )
//--- Underlying-int sentinel for the "Intelligent" SL/TP modes (STOP_LOSS_MODE::SL_INTELLIGENT /
//--- TAKE_PROFIT_MODE::TP_INTELLIGENT, both -1 in Enumerations\InputEnums.mqh). Kept as a local macro
//--- rather than referencing the enum name so this header stays independent of InputEnums.mqh's include
//--- order, exactly like m_confidence_source being an int (see Variables\ConfidenceBridge.mqh).
# define SL_INTELLIGENT_MODE ( -1 )
# define TP_INTELLIGENT_MODE ( -1 )
//--- SL/TP "previous swing" sentinels (STOP_LOSS_MODE::SL_PREV_SWING / TAKE_PROFIT_MODE::TP_PREV_SWING,
//--- both -101). SL sits exactly at the swing; TP targets the opposite swing. Same local-macro rationale.
# define SL_PREV_SWING_MODE ( -101 )
# define TP_PREV_SWING_MODE ( -101 )
//--- Intelligent (AI-confidence) SL/TP shaping, driven by EffectiveConfidence() (a 0..1 magnitude, see
//--- CExpertSignalAIBase::AIConfidence/DBConfidence per Confidence_Source):
//--- - SL starts SL_INTELLIGENT_BASE_MULT beyond the swing and TIGHTENS by up to AI_SL_TIGHTEN_FACTOR
//--- (30%) as confidence -> 1: a high-conviction setup gets a tighter stop, a marginal one keeps the
//--- full ATR cushion. Still floored at MIN_SL_ATR_MULTIPLIER above.
2026-07-25 22:33:45 -04:00
//--- - TP is a multiple of THIS TRADE'S OWN RISK (the final entry-to-stop distance), not of ATR: it
//--- starts at TP_INTELLIGENT_BASE_RR and WIDENS by up to AI_TP_WIDEN_FACTOR (+100%, i.e. 2x) as
//--- confidence -> 1, so RR runs 2.5 (zero confidence) to 5.0 (full conviction).
//--- WHY risk-relative and not ATR-relative: SL is swing-anchored PLUS padding, so its distance
//--- grows with the swing gap, while an ATR-from-entry TP does not. Those two were decoupled when
//--- TP moved off the opposite-swing anchor (commit 0f09588), and nothing re-checked the result
//--- against Min_Risk_Reward_Ratio: with confidence pinned at 0 (i.e. AI disabled - the shipped
//--- default) the old TP_INTELLIGENT_BASE_MULT of 3.0 produced reward = 3*ATR against a risk that
//--- MIN_SL_ATR_MULTIPLIER alone floors at 2*ATR, so `reward < 2.0*risk` was ALWAYS true and
//--- OpenParams() rejected 100% of setups on every symbol and timeframe - the EA could not place a
//--- single trade. Deriving TP from the realised risk restores the coupling the swing-anchored TP
//--- used to provide, and makes the default 1:2 rejection filter satisfiable by construction.
//--- Min_Risk_Reward_Ratio stays a pure REJECTION filter - it is never used to size TP here; it
//--- simply stops firing against this mode unless the user raises it above the base RR.
2026-07-22 13:33:56 -04:00
# define SL_INTELLIGENT_BASE_MULT 3.0
2026-07-25 22:33:45 -04:00
# define TP_INTELLIGENT_BASE_RR 2.5
2026-07-13 03:23:39 -04:00
# define AI_SL_TIGHTEN_FACTOR 0.3
2026-07-22 13:33:56 -04:00
# define AI_TP_WIDEN_FACTOR 1.0
//--- ENTRY_MULTIPLIER "Intelligent"/"Prev swing" sentinels (ENTRY_INTELLIGENT/ENTRY_PREV_SWING in
//--- Enumerations\InputEnums.mqh, -100/-101), kept as local macros for the same include-order
//--- independence as the SL/TP sentinels above. ENTRY_INTELLIGENT_BASE_MULT is the DEEPEST limit
//--- pullback (in ATRs, at zero confidence); it shrinks linearly to 0 (market fill) as confidence -> 1.
# define ENTRY_INTELLIGENT_MODE ( -100 )
# define ENTRY_PREV_SWING_MODE ( -101 )
# define ENTRY_INTELLIGENT_BASE_MULT 2.0
2026-07-13 03:23:39 -04:00
//
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 ) ;
2026-07-29 00:38:05 -04:00
//--- (CheckForDuplicateTrade / FindLastTradeIndex / UpdateTradeStatusAndExit were declared here but
//--- never defined anywhere and never called - removed. Nothing linked against them; they only made
//--- it look as though duplicate-trade detection existed on this class.)
2025-05-30 16:35:54 +02:00
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 ) ;
2026-07-27 11:13:19 -04:00
bool ShouldTraceTradeRejections ( void ) const ;
2026-07-26 23:08:32 -04:00
//--- Mirrors CExpertTrade::Buy()/Sell()'s own price-vs-stops-level decision so OpenParams() can
//--- validate the stops against the order type the trade layer is actually going to send.
ENUM_ORDER_TYPE ResolveOrderType ( bool isLong , double price ) ;
2025-05-30 16:35:54 +02:00
void BufferNewTickSignal ( string filterID , string pattern , string bias , const MqlDateTime & gmtTime , double entryPrice ) ;
2026-07-13 03:23:39 -04:00
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
2026-07-22 13:33:56 -04:00
int m_sl_mode ; // STOP_LOSS_MODE int: >0 = fixed ATR multiple beyond swing; SL_INTELLIGENT(-1) = AI-confidence scaled
int m_tp_mode ; // TAKE_PROFIT_MODE int: >0 = fixed ATR multiple from entry; TP_INTELLIGENT(-1) = AI-confidence scaled
double m_min_risk_reward_ratio ; // Minimum reward:risk to open a trade - REJECTION FILTER ONLY, never shapes TP
2026-07-13 03:23:39 -04:00
int m_confidence_source ; // CONFIDENCE_SOURCE underlying int (0=AI, 1=DB, 2=Blended)
2026-07-26 18:33:12 -04:00
//--- 0..1 min. AI confidence, reversed against the position, required to trigger an early exit. Set
//--- from the SAME Min_Vote_Close input that drives m_threshold_close, just rescaled - see that
//--- input's declaration comment (Variables\Inputs.mqh) for why one number governs both exit routes.
//--- There is deliberately no companion on/off flag: Min_Vote_Close = Disabled resolves to 1.01 here,
//--- which no softmax confidence can reach, so the route switches itself off.
double m_ai_exit_threshold ;
2026-07-13 03:23:39 -04:00
double m_dbConfidence ; // last average normalized DB win-rate across active filters
2026-07-18 02:01:06 -04:00
//--- 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.
2026-07-26 15:31:09 -04:00
//--- The window key is a full GMT timestamp, NOT MqlDateTime.sec. Keying on the 0-59 seconds FIELD
//--- alone made two calls a minute (or an hour, or a day) apart look like the same window: with
//--- Expert_EveryTick=false every call lands on a bar open, where sec is always 0, so the window
//--- never rolled over and every bar's vote accumulated into one ever-growing average that decayed
//--- toward 0 as the run went on. A full timestamp rolls the window over on every new second, which
//--- is what "average the votes cast within one second" was always meant to mean.
datetime m_directionCurrentSecond ;
2026-07-18 02:01:06 -04:00
double m_directionAggregatedResult ;
int m_directionCount ;
double m_directionLastResult ;
2026-07-23 08:21:41 -04:00
int m_lastFiredDirection ; // +1 Buy / -1 Sell / 0 none - THIS filter's own latest vote,
// set in Direction() before children are added in. Unlike
// GetActiveDirection(), never consumed/reset by a read - a
// pure peek, safe for a parent to poll every tick.
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 ; }
2026-07-22 13:33:56 -04:00
void SLMode ( int value ) { m_sl_mode = value ; }
void TPMode ( int value ) { m_tp_mode = value ; }
2026-07-13 03:23:39 -04:00
void MinRiskRewardRatio ( double value ) { m_min_risk_reward_ratio = value ; }
void ConfidenceSource ( int value ) { m_confidence_source = value ; }
void AIExitThreshold ( double value ) { m_ai_exit_threshold = value ; }
2026-07-23 08:21:41 -04:00
int LastFiredDirection ( void ) { return m_lastFiredDirection ; }
2026-07-13 03:23:39 -04:00
// 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 ; }
2026-07-22 13:33:56 -04:00
// Returns this instance's own SignedAIConfidence() when it IS an AI signal, otherwise the live
// value the AI signal publishes each tick (g_LiveAISignedConfidence, see
// CExpertSignalAIBase::ScheduleTrainingIfNeeded). This is what lets the non-AI aggregate/root
// signal - the object CExpert actually calls to size, scale, and manage every trade - see REAL AI
// confidence instead of the constant 0 its own SignedAIConfidence() returns. Without it,
// Intelligent MM, AI SL/TP scaling, and AI-exit were all running with their AI component pinned to 0.
double LiveSignedConfidence ( void ) ;
2026-07-13 03:23:39 -04:00
// 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 ;
2026-07-26 17:09:13 -04:00
//--- Vote lifecycle hooks, for filters whose LongCondition()/ShortCondition() consume one-shot state
//--- when they fire (today: CExpertSignalAIBase's m_lastNonNeutralSignal alternation gate). Direction()
//--- calls BeginVote() on itself before polling its own conditions, and RevokeVote() on any CHILD whose
//--- vote it then throws away. Without this, a vote that Hybrid's quorum suppressed still burned the
//--- child's gate: PAI flipping Buy alone on bar 10 consumed its Buy gate, so when CONV flipped Buy on
//--- bar 12 PAI was already gated to 0 and the count was STILL 1 of the 2 required - in practice all
//--- three models had to flip on the very same bar, and every near-miss cost a model that direction
//--- until the opposite signal arrived. Deliberately NOT revoked on the prohibition path: a vetoed tick
//--- still blocks only OPENING (see CheckOpenPosition), and the vote does reach m_direction where
//--- CheckClosePosition can act on it, so that vote was used, not discarded. Base = no-op.
virtual void BeginVote ( void ) { }
virtual void RevokeVote ( void ) { }
2025-05-30 16:35:54 +02:00
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 ) ,
2026-07-13 03:23:39 -04:00
m_useDatabase ( false ) ,
2026-07-22 13:33:56 -04:00
m_sl_mode ( 3 ) , // SL_ATR_x3
m_tp_mode ( 6 ) , // TP_ATR_x6
2026-07-13 03:23:39 -04:00
m_min_risk_reward_ratio ( 2.0 ) ,
m_confidence_source ( 0 ) ,
2026-07-26 18:33:12 -04:00
//--- seeded unreachable (>1.0), so an instance whose AIExitThreshold() was never set from
//--- Min_Vote_Close cannot early-exit on a stale default rather than on the trader's setting
m_ai_exit_threshold ( 1.01 ) ,
2026-07-18 02:01:06 -04:00
m_dbConfidence ( 0.0 ) ,
2026-07-26 15:31:09 -04:00
m_directionCurrentSecond ( 0 ) ,
2026-07-18 02:01:06 -04:00
m_directionAggregatedResult ( 0.0 ) ,
m_directionCount ( 0 ) ,
2026-07-23 08:21:41 -04:00
m_directionLastResult ( 0.0 ) ,
2026-07-27 22:08:55 -04:00
m_lastFiredDirection ( 0 )
2026-07-13 03:23:39 -04:00
{
}
//+------------------------------------------------------------------+
//| Combine AI/DB confidence per the configured Confidence_Source |
//+------------------------------------------------------------------+
2026-07-22 13:33:56 -04:00
double CExpertSignalCustom : : LiveSignedConfidence ( void )
{
double own = SignedAIConfidence ( ) ;
return ( own ! = 0.0 ) ? own : g_LiveAISignedConfidence ;
}
2026-07-13 03:23:39 -04:00
double CExpertSignalCustom : : EffectiveConfidence ( void )
2025-05-30 16:35:54 +02:00
{
2026-07-22 13:33:56 -04:00
g_AISignedConfidence = LiveSignedConfidence ( ) ;
2026-07-13 03:23:39 -04:00
g_DBConfidence = m_dbConfidence ;
return CombinedConfidence ( m_confidence_source ) ;
2025-05-30 16:35:54 +02:00
}
//+------------------------------------------------------------------+
//| Destructor |
//+------------------------------------------------------------------+
CExpertSignalCustom : : ~ CExpertSignalCustom ( void )
{
ArrayFree ( signalBuffer ) ;
}
//+------------------------------------------------------------------+
2026-07-27 11:13:19 -04:00
//| Tester-only trade rejection tracing |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : ShouldTraceTradeRejections ( void ) const
{
2026-07-27 15:52:39 -04:00
return VerboseMode ;
}
void TraceSignalRejection ( const string key , const string message )
{
if ( ! VerboseMode )
return ;
TCLog ( " signal-reject: " + key , message ) ;
2026-07-27 11:13:19 -04:00
}
//+------------------------------------------------------------------+
2026-07-13 03:23:39 -04:00
//| 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 + + )
{
2026-07-13 03:23:39 -04:00
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 ;
}
//+------------------------------------------------------------------+
2026-07-26 23:08:32 -04:00
//| Which order type a given entry price will actually produce. |
//| CExpertTrade::Buy()/Sell() route on price vs ask/bid +- the |
//| SYMBOL_TRADE_STOPS_LEVEL: further out than that in the pending |
//| direction becomes a stop/limit order, anything nearer becomes a |
//| market fill. Reproducing that decision here (rather than assuming |
//| "Entry_Multiplier != MARKET means pending") is what lets |
//| OpenParams() validate the SL/TP against the right reference |
//| price - the article measures a market order's stops from the |
//| OPPOSITE side of the spread and a pending order's from its own |
//| activation price, and those are different numbers. |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE CExpertSignalCustom : : ResolveOrderType ( bool isLong , double price )
{
if ( price < = 0.0 )
return ( isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL ) ;
double stops = TCStopsLevel ( m_symbol . Name ( ) ) ;
if ( isLong )
{
double ask = m_symbol . Ask ( ) ;
if ( price > ask + stops )
return ( ORDER_TYPE_BUY_STOP ) ;
if ( price < ask - stops )
return ( ORDER_TYPE_BUY_LIMIT ) ;
return ( ORDER_TYPE_BUY ) ;
}
double bid = m_symbol . Bid ( ) ;
if ( price > bid + stops )
return ( ORDER_TYPE_SELL_LIMIT ) ;
if ( price < bid - stops )
return ( ORDER_TYPE_SELL_STOP ) ;
return ( ORDER_TYPE_SELL ) ;
}
//+------------------------------------------------------------------+
2025-05-30 16:35:54 +02:00
//| 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 ) ;
2026-07-27 15:52:39 -04:00
if ( ! MathIsValidNumber ( atr ) | | atr < = 0.0 )
2025-05-30 16:35:54 +02:00
return false ; // ATR must be positive
if ( ! m_symbol . Name ( _Symbol ) )
return false ; // Symbol information must be accessible
2026-07-26 23:08:32 -04:00
//--- Article 2555 #14: every symbol-property read below (stops level, point, digits) silently
//--- returns 0 for a symbol that is not selected/quoted, which would turn each of the checks
//--- further down into an unconditional pass. Verify the symbol is real and quoted first.
string tc_reason ;
if ( ! TCSymbolIsTradeable ( m_symbol . Name ( ) , tc_reason ) )
{
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " openparams-symbol: " + m_symbol . Name ( ) ,
__FUNCTION__ + " : rejected - " + tc_reason ) ;
2026-07-26 23:08:32 -04:00
return false ;
}
2025-05-30 16:35:54 +02:00
int lookback_period = m_periods ;
2026-07-26 23:08:32 -04:00
//--- Article 2555 #8: iLowest/iHighest below scan `lookback_period` bars starting at `idx`, and
//--- the ATR read above needs its own warm-up. Rather than discovering the shortfall as a -1
//--- index (handled below) or as a silently truncated scan, check the series depth up front and
//--- let the terminal build the missing history - the next tick finds it ready.
if ( ! TCHasEnoughHistory ( m_symbol . Name ( ) , m_period , lookback_period + idx + m_periods , tc_reason ) )
{
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " openparams-history: " + m_symbol . Name ( ) ,
__FUNCTION__ + " : rejected - " + tc_reason ) ;
2026-07-26 23:08:32 -04:00
return false ;
}
2025-05-30 16:35:54 +02:00
double base_price = ( m_base_price = = 0.0 ) ? ( isLong ? m_symbol . Ask ( ) : m_symbol . Bid ( ) ) : m_base_price ;
2026-07-27 15:52:39 -04:00
if ( ! MathIsValidNumber ( base_price ) | | base_price < = 0.0 )
return false ; // Price feed must be valid
// Keep swing sourcing strictly bound to this signal's symbol/timeframe. Mixing chart globals
// here can yield index/value mismatches in tester runs and diverge from classic behavior.
int lowest_index = iLowest ( m_symbol . Name ( ) , m_period , MODE_LOW , lookback_period , idx ) ;
int highest_index = iHighest ( m_symbol . Name ( ) , m_period , MODE_HIGH , lookback_period , idx ) ;
2026-07-26 12:12:14 -04:00
if ( lowest_index < 0 | | highest_index < 0 )
{
// iLowest/iHighest return -1 when the requested history isn't synced yet (thin symbol history,
// timeframe just changed, broker feed gap). Indexing Low()/High() with -1 would otherwise feed
// a bogus swing price into SL/TP below - reject the setup instead.
2026-07-27 15:52:39 -04:00
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " openparams-swing-index: " + m_symbol . Name ( ) ,
__FUNCTION__ + " : rejected - iLowest/iHighest returned an invalid index (lowest= " + IntegerToString ( lowest_index ) +
" , highest= " + IntegerToString ( highest_index ) + " ) for " + m_symbol . Name ( ) + " , insufficient history synced. " ) ;
return false ;
}
double lowest_low = iLow ( m_symbol . Name ( ) , m_period , lowest_index ) ;
double highest_high = iHigh ( m_symbol . Name ( ) , m_period , highest_index ) ;
if ( lowest_low > = DBL_MAX * 0.5 | | highest_high > = DBL_MAX * 0.5 )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " openparams-swing-sentinel: " + m_symbol . Name ( ) ,
StringFormat ( " %s: rejected - swing prices are sentinel-like (lowest_low=%g, highest_high=%g, symbol=%s, period=%d, low_idx=%d, high_idx=%d). " ,
__FUNCTION__ , lowest_low , highest_high , m_symbol . Name ( ) , m_period , lowest_index , highest_index ) ) ;
return false ;
}
if ( ! MathIsValidNumber ( lowest_low ) | | ! MathIsValidNumber ( highest_high ) )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " openparams-swing-nonfinite: " + m_symbol . Name ( ) ,
StringFormat ( " %s: rejected - swing prices are not finite (lowest_low=%g, highest_high=%g, symbol=%s, period=%d). " ,
__FUNCTION__ , lowest_low , highest_high , m_symbol . Name ( ) , m_period ) ) ;
return false ;
}
if ( lowest_low < = 0.0 | | highest_high < = 0.0 )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " openparams-swing-nonpositive: " + m_symbol . Name ( ) ,
StringFormat ( " %s: rejected - swing prices are non-positive (lowest_low=%g, highest_high=%g, symbol=%s, period=%d). " ,
__FUNCTION__ , lowest_low , highest_high , m_symbol . Name ( ) , m_period ) ) ;
2026-07-26 12:12:14 -04:00
return false ;
}
2026-07-22 13:33:56 -04:00
double minRR = m_min_risk_reward_ratio ; // REJECTION threshold only - never used to size TP
// Refresh the confidence bridge every tick regardless of SL/TP mode, so Intelligent MM
// (Money\MoneyIntelligent.mqh), the intelligent trailing (Trailing\TrailingIntelligent.mqh), and
// intelligent entry below all see a fresh value even when SL/TP are left on fixed-ATR presets.
2026-07-13 03:23:39 -04:00
double confidence = EffectiveConfidence ( ) ;
2026-07-27 15:52:39 -04:00
if ( ! MathIsValidNumber ( confidence ) )
confidence = 0.0 ;
2026-07-22 13:33:56 -04:00
// --- Entry price. Offsets are measured from the CURRENT price (base_price = bid/ask), except
// ENTRY_PREV_SWING which anchors to the recent swing. The resulting price is what
// CExpertTrade::Buy/Sell routes into a market / limit / stop order (it compares price to
// ask/bid +- the broker stop-level itself), so a near-market price simply fills at market.
int entryMode = ( int ) m_entry_multiplier ;
if ( entryMode = = ENTRY_PREV_SWING_MODE )
price = m_symbol . NormalizePrice ( isLong ? lowest_low : highest_high ) ;
else if ( entryMode = = ENTRY_INTELLIGENT_MODE )
2026-07-13 03:23:39 -04:00
{
2026-07-22 13:33:56 -04:00
// Deep limit pullback when unsure, shrinking to a market fill as confidence -> 1.
double pull = ENTRY_INTELLIGENT_BASE_MULT * ( 1.0 - confidence ) * atr ;
price = m_symbol . NormalizePrice ( isLong ? ( base_price - pull ) : ( base_price + pull ) ) ;
2025-05-30 16:35:54 +02:00
}
2026-07-22 13:33:56 -04:00
else
// Fixed ATR presets: buy => base + mult*ATR (limit below / stop above for -/+ mult);
// sell => base - mult*ATR (limit above / stop below). MARKET (0) leaves price at bid/ask.
price = m_symbol . NormalizePrice ( isLong ? ( base_price + entryMode * atr ) : ( base_price - entryMode * atr ) ) ;
// --- Stop loss: always swing-anchored. SL_ATR_* pad the swing by that many ATR; SL_INTELLIGENT
// tightens the pad as confidence rises; SL_PREV_SWING sits EXACTLY at the swing (zero pad).
double slMultiplier ;
if ( m_sl_mode = = SL_INTELLIGENT_MODE )
slMultiplier = SL_INTELLIGENT_BASE_MULT * ( 1.0 - AI_SL_TIGHTEN_FACTOR * confidence ) ;
else if ( m_sl_mode = = SL_PREV_SWING_MODE )
slMultiplier = 0.0 ;
else
slMultiplier = ( double ) m_sl_mode ;
sl = isLong ? m_symbol . NormalizePrice ( lowest_low - slMultiplier * atr )
: m_symbol . NormalizePrice ( highest_high + slMultiplier * atr ) ;
2026-07-25 22:33:45 -04:00
// Enforce a hard minimum SL distance from entry (broker stop-level / sanity floor). Deliberately
// applied BEFORE take profit below: TP_INTELLIGENT sizes itself off the FINAL entry-to-stop distance,
// so a floor that widened the stop afterwards would silently shrink the realised reward:risk below the
// ratio that mode is meant to guarantee - and, at the shipped defaults, straight back under the Min RR
// rejection threshold.
if ( fabs ( price - sl ) < ( MIN_SL_ATR_MULTIPLIER * atr ) )
sl = isLong ? ( price - MIN_SL_ATR_MULTIPLIER * atr ) : ( price + MIN_SL_ATR_MULTIPLIER * atr ) ;
double risk = fabs ( price - sl ) ;
// --- Take profit: TP_ATR_* are an ATR multiple FROM THE ENTRY PRICE; TP_INTELLIGENT is a multiple of
// THIS TRADE'S OWN RISK, widening with confidence (see TP_INTELLIGENT_BASE_RR's comment for why
// it is risk-relative rather than ATR-relative); TP_PREV_SWING instead targets the opposite recent
// swing (buy: swing high / sell: swing low). Min RR (below) only rejects, never reshapes any of these.
2026-07-22 13:33:56 -04:00
if ( m_tp_mode = = TP_PREV_SWING_MODE )
tp = isLong ? m_symbol . NormalizePrice ( highest_high ) : m_symbol . NormalizePrice ( lowest_low ) ;
2025-05-30 16:35:54 +02:00
else
2026-07-25 22:33:45 -04:00
if ( m_tp_mode = = TP_INTELLIGENT_MODE )
{
double targetRR = TP_INTELLIGENT_BASE_RR * ( 1.0 + AI_TP_WIDEN_FACTOR * confidence ) ;
tp = isLong ? m_symbol . NormalizePrice ( price + targetRR * risk )
: m_symbol . NormalizePrice ( price - targetRR * risk ) ;
}
else
{
double tpMultiplier = ( double ) m_tp_mode ;
tp = isLong ? m_symbol . NormalizePrice ( price + tpMultiplier * atr )
: m_symbol . NormalizePrice ( price - tpMultiplier * atr ) ;
}
2026-07-27 15:52:39 -04:00
// Guard rail: when both AI and classic share this path, any non-finite or negative level here is an
// upstream data/state issue, not a mode-specific feature. Reject early with full context.
if ( ! MathIsValidNumber ( price ) | | price < 0.0 | |
! MathIsValidNumber ( sl ) | | sl < 0.0 | |
! MathIsValidNumber ( tp ) | | tp < 0.0 )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " openparams-invalid-levels: " + m_symbol . Name ( ) ,
StringFormat ( " %s: rejected - invalid computed levels (isLong=%s, entryMode=%d, slMode=%d, tpMode=%d, atr=%g, base=%g, low=%g, high=%g, price=%g, sl=%g, tp=%g). " ,
__FUNCTION__ , isLong ? " true " : " false " , entryMode , m_sl_mode , m_tp_mode ,
atr , base_price , lowest_low , highest_high , price , sl , tp ) ) ;
return false ;
}
2026-07-26 23:08:32 -04:00
// --- Article 2555 #6: SL and TP must clear SYMBOL_TRADE_STOPS_LEVEL, measured against the price of
// the OPPOSITE operation for a market order (a long closes at Bid, a short at Ask) or against
// the activation price for a pending one. Nothing upstream enforced this: SL is anchored to a
// recent swing and TP to an ATR/RR multiple, both of which can land inside the broker's minimum
// distance on a quiet bar or a wide-spread symbol - the trade was then built, sized by Money,
// and rejected server-side with "Invalid stops" (10016) with nothing in the log explaining why.
// Which order type this becomes is decided by CExpertTrade::Buy()/Sell() purely from `price` vs
// ask/bid +- the stops level, so the same comparison is reproduced here to pick the type the
// stops will actually be validated against.
ENUM_ORDER_TYPE order_type = ResolveOrderType ( isLong , price ) ;
string stops_note ;
if ( ! TCAdjustStops ( m_symbol . Name ( ) , order_type , price , sl , tp , stops_note ) )
{
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " openparams-stops: " + m_symbol . Name ( ) , __FUNCTION__ + " : rejected - " + stops_note ) ;
2026-07-26 23:08:32 -04:00
return false ;
}
if ( stops_note ! = " " )
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " openparams-stops-adj: " + m_symbol . Name ( ) , __FUNCTION__ + " : " + stops_note ) ;
2026-07-26 23:08:32 -04:00
// A widened stop changes this trade's real risk, so recompute it before the reward:risk filter
// below - otherwise the RR the trade is accepted on is not the RR it is actually taken at.
risk = fabs ( price - sl ) ;
// Re-verify rather than trust the correction: TCAdjustStops() widens levels, and a caller that
// hands it a nonsensical pair (SL on the wrong side of the entry) can still come back illegal.
if ( ! TCCheckStops ( m_symbol . Name ( ) , order_type , price , sl , tp , stops_note ) )
{
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " openparams-stops-final: " + m_symbol . Name ( ) , __FUNCTION__ + " : rejected - " + stops_note ) ;
2026-07-26 23:08:32 -04:00
return false ;
}
// A pending order's own activation price is subject to the same minimum distance. If `price`
// drifted inside it between the entry calculation above and now, CExpertTrade would quietly
// downgrade the order to a market fill at a price the setup never asked for - reject instead.
if ( order_type ! = ORDER_TYPE_BUY & & order_type ! = ORDER_TYPE_SELL & &
! TCCheckPendingPrice ( m_symbol . Name ( ) , order_type , price , stops_note ) )
{
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " openparams-pending: " + m_symbol . Name ( ) , __FUNCTION__ + " : rejected - " + stops_note ) ;
2026-07-26 23:08:32 -04:00
return false ;
}
// Article 2555 #4: a pending order also has to fit inside ACCOUNT_LIMIT_ORDERS. Checked here,
// before the setup is handed to Money for sizing, so a full order book costs nothing downstream.
if ( order_type ! = ORDER_TYPE_BUY & & order_type ! = ORDER_TYPE_SELL & &
! TCIsNewOrderAllowed ( stops_note ) )
{
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " openparams-orderlimit " , __FUNCTION__ + " : rejected - " + stops_note ) ;
2026-07-26 23:08:32 -04:00
return false ;
}
2026-07-22 13:33:56 -04:00
// Min reward:risk is now ONLY a rejection filter (Min_Risk_Reward_Ratio) - it never reshapes TP.
2025-05-30 16:35:54 +02:00
double reward = fabs ( tp - price ) ;
2026-07-18 17:39:58 -04:00
// 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 ;
2026-07-13 03:23:39 -04:00
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 ;
2026-07-26 18:33:12 -04:00
// 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. LiveSignedConfidence()
// supplies the AI signal's live value even on the non-AI aggregate/root signal this runs on, so this is
// a no-op when no AI signal is active/converged yet (it returns 0.0) or when Min_Vote_Close is Disabled
// (m_ai_exit_threshold resolves to 1.01, which no confidence magnitude can reach).
//
// This is NOT redundant with the averaged vote above, which is why it exists as a second route rather
// than being folded into it. The AI's ordinary vote is ONE-SHOT - LongCondition()/ShortCondition()
// consume the m_lastNonNeutralSignal alternation gate the moment they fire - and it is then AVERAGED
// with every other filter's. So an AI reversal that lands on a bar where the average stays under
// m_threshold_close has already burned its gate and will never be re-offered, leaving the position open
// for as long as the AI holds that (now un-votable) view. Reading the LIVE signed confidence here,
// undiluted and every bar, is what closes that hole.
if ( ! result )
2026-07-13 03:23:39 -04:00
{
2026-07-22 13:33:56 -04:00
double signed_conf = LiveSignedConfidence ( ) ;
2026-07-13 03:23:39 -04:00
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 )
2026-07-27 11:13:19 -04:00
{
if ( ShouldTraceTradeRejections ( ) )
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " open-prohibition " ,
StringFormat ( " %s: open %s rejected - a child filter vetoed the tick (prohibition signal). " ,
__FUNCTION__ , isLong ? " long " : " short " ) ) ;
2026-07-27 11:13:19 -04:00
return false ;
}
2025-05-30 16:35:54 +02:00
//--- 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
2026-07-27 11:13:19 -04:00
if ( ! ( isLong ? OpenLongParams ( price , sl , tp , expiration ) : OpenShortParams ( price , sl , tp , expiration ) ) )
{
2026-07-27 15:52:39 -04:00
// The vote reached the threshold but entry-shaping failed (invalid SL/TP, broker constraints,
// missing history). Roll back one-shot child vote state so the same directional signal can
// be re-offered on the next bar instead of being permanently consumed by this failed attempt.
int total = m_filters . Total ( ) ;
for ( int i = 0 ; i < total ; i + + )
{
CExpertSignalCustom * filter = m_filters . At ( i ) ;
if ( filter ! = NULL )
filter . RevokeVote ( ) ;
}
RevokeVote ( ) ;
2026-07-27 11:13:19 -04:00
if ( ShouldTraceTradeRejections ( ) )
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " open-params-failed " ,
StringFormat ( " %s: open %s rejected after direction passed threshold - order parameters failed validation (vote state restored for retry). " ,
__FUNCTION__ , isLong ? " long " : " short " ) ) ;
2025-05-30 16:35:54 +02:00
result = false ;
2026-07-27 11:13:19 -04:00
}
2025-05-30 16:35:54 +02:00
}
2026-07-27 11:13:19 -04:00
else if ( ShouldTraceTradeRejections ( ) )
{
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " open-threshold " ,
StringFormat ( " %s: open %s rejected - direction %.2f did not reach threshold %.2f. " ,
__FUNCTION__ , isLong ? " long " : " short " , directionMultiplier * m_direction , m_threshold_open ) ) ;
2026-07-27 11:13:19 -04:00
}
2025-05-30 16:35:54 +02:00
//--- 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
2026-07-27 11:13:19 -04:00
if ( ShouldTraceTradeRejections ( ) )
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " open-long-direction-block " ,
StringFormat ( " %s: open long rejected - strategy direction blocks long entries. " , __FUNCTION__ ) ) ;
2025-05-30 16:35:54 +02:00
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
2026-07-27 11:13:19 -04:00
if ( ShouldTraceTradeRejections ( ) )
2026-07-27 15:52:39 -04:00
TraceSignalRejection ( " open-short-direction-block " ,
StringFormat ( " %s: open short rejected - strategy direction blocks short entries. " , __FUNCTION__ ) ) ;
2025-05-30 16:35:54 +02:00
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 ;
2026-07-26 15:31:09 -04:00
datetime nowGMT = TimeGMT ( gmtTime ) ; // full timestamp AND broken-down form - both are used below
//--- Open a fresh intra-second averaging window whenever the second changes. This block may ONLY
//--- reset the window - it must never be the thing that publishes m_directionLastResult. It used to
//--- close the previous window here and return that value, which meant the value handed to
//--- CExpert(Custom)::SetDirection() -> m_direction (the field CheckOpenPosition/CheckClosePosition
//--- actually threshold against) was always the PREVIOUS second's average, never this call's own
//--- vote. With Expert_EveryTick=false, Direction() runs exactly once per bar at the bar open, so
//--- TimeGMT().sec is 0 on every single call: after the very first call the branch below never fired
//--- again, m_directionLastResult stayed pinned at its 0.0 seed forever, and m_direction was 0 on
//--- every bar - no signal could ever reach m_threshold_open and the EA could not open a single
//--- trade, in Classic, AI-only or Hybrid alike (they all inherit this one Direction() body). It also
//--- silently ate the AI vote entirely: CExpertSignalAIBase::LongCondition/ShortCondition consume the
//--- one-shot alternation gate (m_lastNonNeutralSignal) when they fire, so the discarded vote was
//--- never re-offered on a later bar. The window average is now computed at the end of this function
//--- with this call's own result folded in, so what is returned always includes the current tick.
if ( nowGMT ! = m_directionCurrentSecond )
2025-05-30 16:35:54 +02:00
{
2026-07-18 02:01:06 -04:00
m_directionAggregatedResult = 0.0 ;
m_directionCount = 0 ;
2026-07-26 15:31:09 -04:00
m_directionCurrentSecond = nowGMT ; // Update the current second
2025-05-30 16:35:54 +02:00
}
m_prohibition_signal = false ;
2026-07-26 17:09:13 -04:00
BeginVote ( ) ; // snapshot any one-shot vote state, so a discarded vote can be rolled back - see BeginVote()
2025-05-30 16:35:54 +02:00
double result = m_weight * ( LongCondition ( ) - ShortCondition ( ) ) ;
2026-07-23 08:21:41 -04:00
//--- Non-consuming quorum peek - see m_lastFiredDirection's declaration comment. Snapshotted from
//--- this filter's OWN vote, before the loop below adds any children's contributions in.
m_lastFiredDirection = ( result > 0.0 ) ? 1 : ( ( result < 0.0 ) ? -1 : 0 ) ;
2025-05-30 16:35:54 +02:00
int number = ( result = = 0.0 ) ? 0 : 1 ;
int total = m_filters . Total ( ) ;
PrintVerbose ( " Starting direction calculation with total filters: " + IntegerToString ( total ) ) ;
2026-07-23 08:21:41 -04:00
//--- Pass 1: refresh every filter's own Direction() - required regardless of quorum, since this is
//--- what drives each filter's own training/DB-buffering/m_lastFiredDirection side effects - caching
//--- the returned magnitude for pass 2 below instead of summing it immediately. Quorum suppression
//--- (pass 2) needs every quorum-flagged filter's m_lastFiredDirection already fresh for THIS tick;
//--- checking mid-loop, as a single pass used to, would compare against filters not yet visited this
//--- iteration (stale, still holding last tick's value).
double directions [ ] ;
ArrayResize ( directions , total ) ;
bool aborted = false ;
2025-05-30 16:35:54 +02:00
for ( int i = 0 ; i < total ; i + + )
{
long mask = ( ( long ) 1 ) < < i ;
if ( ( m_ignore & mask ) ! = 0 )
2026-07-23 08:21:41 -04:00
{
directions [ i ] = EMPTY_VALUE ;
2025-05-30 16:35:54 +02:00
continue ;
2026-07-23 08:21:41 -04:00
}
2025-05-30 16:35:54 +02:00
CExpertSignalCustom * filter = m_filters . At ( i ) ;
if ( filter = = NULL )
{
Print ( " Error: Filter at index " + IntegerToString ( i ) + " is NULL " ) ;
2026-07-23 08:21:41 -04:00
directions [ i ] = EMPTY_VALUE ;
2025-05-30 16:35:54 +02:00
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 ( ) ;
2026-07-26 17:09:13 -04:00
//--- Only journal a pattern when the label AGREES with the net vote the filter actually cast.
//--- m_active_direction/m_active_pattern are last-writer-wins across LongCondition() then
//--- ShortCondition(), and both sides can fire on the same bar - e.g. CSignalMA with close below
//--- its MA returns Pattern_1 long AND Pattern_0 short, netting to a vote of 0 while the labels
//--- read "Sell"/"Pattern_0". Buffering off the labels alone therefore recorded a directional
//--- pattern for a bar the filter voted FLAT on, poisoning the very win-rate table
//--- UpdateSignalsWeights() feeds back into that pattern's weight. LastFiredDirection() is the
//--- signed net vote, set in this filter's own Direction(); like the labels it is read here one
//--- tick after being written, so the two are compared as of the same tick.
int filterVote = filter . LastFiredDirection ( ) ;
bool labelMatchesVote = ( bias = = " Buy " & & filterVote > 0 ) | | ( bias = = " Sell " & & filterVote < 0 ) ;
if ( filterID ! = " NULL " & & bias ! = " NULL " & & pattern ! = " NULL " & & m_useDatabase & & labelMatchesVote )
2025-05-30 16:35:54 +02:00
{
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 ;
2026-07-23 08:21:41 -04:00
directions [ i ] = EMPTY_VALUE ;
2025-05-30 16:35:54 +02:00
continue ;
}
2026-07-23 08:21:41 -04:00
// Validate the result to be within the range of -100 to 100
if ( direction < -100 | | direction > 100 )
2025-05-30 16:35:54 +02:00
{
2026-07-23 08:21:41 -04:00
PrintVerbose ( " A filter's direction is invalid. Skipping tick. " ) ;
result = 0 ;
number = 0 ;
aborted = true ;
break ;
}
directions [ i ] = direction ;
}
2026-07-26 17:09:13 -04:00
//--- The tick was discarded, so NO filter's vote was used - roll every one of them back, for the same
//--- reason a quorum-suppressed vote is rolled back in pass 2 below (see BeginVote()/RevokeVote()).
if ( aborted )
{
for ( int i = 0 ; i < total ; i + + )
{
CExpertSignalCustom * filter = m_filters . At ( i ) ;
if ( filter ! = NULL )
filter . RevokeVote ( ) ;
}
}
2026-07-27 22:08:55 -04:00
//--- Pass 2: sum each filter's cached contribution. Standard weighted voting only - no quorum gate.
2026-07-23 08:21:41 -04:00
if ( ! aborted )
{
for ( int i = 0 ; i < total ; i + + )
{
double direction = directions [ i ] ;
if ( direction = = EMPTY_VALUE | | direction = = 0 )
continue ;
CExpertSignalCustom * filter = m_filters . At ( i ) ;
number + + ; // Only increment `number` if `direction` is not 0 or EMPTY_VALUE and not suppressed
long mask = ( ( long ) 1 ) < < i ;
result + = ( ( m_invert & mask ) ! = 0 ) ? - direction : direction ;
2025-05-30 16:35:54 +02:00
}
}
2026-07-26 15:31:09 -04:00
//--- Normalization, as CExpertSignal::Direction() does it: the weighted votes are AVERAGED over the
//--- filters that actually voted, not summed. `number` was being counted here and then never used,
//--- which left result as a raw sum - two ordinary agreeing votes (e.g. MA's 60 + RSI's 100) could
//--- exceed the +-100 valid band and get zeroed by the range check below, throwing away exactly the
//--- strongest, most agreed-upon setups. Only non-zero, non-suppressed contributions increment
//--- `number` (see pass 2), so a lone filter voting 10 still normalizes to 10 and can clear a
//--- ThresholdOpen(10) on its own - averaging does not raise the bar for a single-voter signal.
if ( ! aborted & & number ! = 0 )
result / = number ;
//--- Fold this call's result into the current second's window and publish the window average - see
//--- the window-reset block at the top of this function for why this must happen here.
2026-07-18 02:01:06 -04:00
m_directionAggregatedResult + = result ;
m_directionCount + + ;
2026-07-26 15:31:09 -04:00
m_directionLastResult = m_directionAggregatedResult / m_directionCount ;
2025-05-30 16:35:54 +02:00
// Validate the aggregated result to be within the range of -100 to 100
2026-07-18 02:01:06 -04:00
if ( m_directionLastResult < -100 | | m_directionLastResult > 100 )
2025-05-30 16:35:54 +02:00
{
2026-07-18 02:01:06 -04: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. " ) ;
}
2026-07-18 02:01:06 -04:00
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 ;
}
2026-07-13 03:23:39 -04:00
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 )
{
2026-07-13 03:23:39 -04:00
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 ( ) )
{
2026-07-26 12:12:14 -04:00
Print ( __FUNCTION__ + " : Failed to begin database transaction, " + IntegerToString ( ArraySize ( signalBuffer ) ) + " buffered signal(s) left pending for retry next cycle. " ) ;
2025-05-30 16:35:54 +02:00
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 ( ) )
{
2026-07-26 12:12:14 -04:00
Print ( __FUNCTION__ + " : Failed to commit the transaction to the database, rolling back. " + IntegerToString ( ArraySize ( signalBuffer ) ) + " buffered signal(s) left pending for retry next cycle. " ) ;
dbm . RollbackTransaction ( ) ;
2025-05-30 16:35:54 +02:00
return ;
}
ArrayResize ( signalBuffer , 0 ) ;
PrintVerbose ( " Signal buffer cleared after processing. " ) ;
2026-07-26 12:12:14 -04:00
// NOTE: does NOT close dbm here - the caller (CExpertCustom::OnTimer) opens the shared
// connection once and also calls UpdateSignalsWeights() right after this returns; closing it
// here made UpdateSignalsWeights() silently fail (BeginTransaction on a closed handle) in every
// live/demo run (IsBacktesting only skipped this close in the tester, masking the bug there).
// The opener (OnTimer) now owns closing it.
2025-05-30 16:35:54 +02:00
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
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 [ ] )
{
2026-07-13 03:23:39 -04:00
return ArraySize ( tradeRecords ) > = MAX_TABLE_ROWS ;
2025-05-30 16:35:54 +02:00
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : DeleteOldestEntry ( string tableName )
{
2026-07-13 03:23:39 -04:00
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 } ;
2026-07-13 03:23:39 -04:00
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 ( ) ;
2026-07-13 03:23:39 -04:00
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
2026-07-13 03:23:39 -04:00
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 ) ;
2026-07-13 03:23:39 -04:00
// 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 ) ) ;
}
2026-07-13 03:23:39 -04:00
if ( validPatternCount > 0 )
{
sumModuleWeight + = normalizedWinRate ;
weightedFilterCount + + ;
}
2025-05-30 16:35:54 +02:00
}
2026-07-13 03:23:39 -04: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 )
{
2026-07-13 03:23:39 -04:00
datetime nowGmt = StructToTime ( gmtTime ) ;
2025-05-30 16:35:54 +02:00
for ( int i = ArraySize ( tr ) - 1 ; i > = 0 ; i - - )
{
2026-07-13 03:23:39 -04:00
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 " )
{
2026-07-22 22:51:04 -04:00
//--- was previously also requiring hour/day/day_of_week/month to all match the CURRENT moment
//--- (gmtTime) simultaneously - a coincidence real trade history essentially never satisfies,
//--- which made this always return NO_DATA_WIN_RATE regardless of actual history. Win rate is
//--- per-pattern, not per-exact-timestamp, so the pattern/result match above is the only filter.
totalTrades + + ; // Increment total trades
if ( tr [ i ] . result = = " Profit " )
profitableTrades + + ;
2025-05-30 16:35:54 +02:00
}
}
// Check if total trades meet the minimum requirement
2026-07-13 03:23:39 -04:00
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 ) ;
}
}
//+------------------------------------------------------------------+