Warrior_EA/Expert/ExpertSignalCustom.mqh
AnimateDread b91c7b1f7a refactor(comments): box headers to stdlib length
The //| box blocks were excluded from 0b06f8e and 5efdb48 and were what
remained: 160 of them ran to 10+ lines, the longest to 88. Compressed to their
leading topic sentences - 5 lines for a function header, 8 for a file header -
keeping the box format and the standard MQL5 name/author lines verbatim.

Verified at the BYTE level this time, across every in-scope file: the list of
non-comment lines is byte-identical to HEAD and braces balance. The first check
compared a locale-decoded 'git show' against a UTF-8 read and flagged 25 files
that had not changed at all - every BOM and every non-ASCII line mismatched.

47,696 -> 40,665 lines in scope; comment share 38% -> 26%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:30:14 -04:00

2177 lines
113 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"
#include "..\System\TradeChecks.mqh"
#include "..\System\BinomialStats.mqh"
//--- Enumerations
#include "..\Enumerations\GlobalEnums.mqh"
//+------------------------------------------------------------------+
//| SIGNAL ARROW NAMESPACE - declared HERE, in the common base, and |
//| not in ExpertSignalAIBase.mqh where it used to live. |
//+------------------------------------------------------------------+
#ifndef SIG_ARROW_PREFIX
#define SIG_ARROW_PREFIX "WarSig_"
#endif
//--- THE FILTERED VIEW's own namespace: the combined vote, which belongs to no single filter. Sits
//--- under the same bare prefix as the per-filter arrows so one purge still reaches everything.
#define SIG_VOTE_PREFIX SIG_ARROW_PREFIX "VOTE_"
//--- SIGNAL MARKS ARE TWO OBJECTS, drawn as a pair for two different reading distances (2026-08-20
//--- user request). The LINE is a short horizontal segment at the trigger price - the precise
//--- entry/ exit level, readable only zoomed in.
#define WARRIOR_SIG_BUY_COLOR clrDodgerBlue
#define WARRIOR_SIG_SELL_COLOR clrRed
//--- THE COLOUR IS THE DIRECTION ENCODING, not decoration - a signal line carries no arrow code, so
//--- SaveChartSignals recovers buy-vs-sell by comparing against WARRIOR_SIG_BUY_COLOR. Half-width
//--- of the segment as a fraction of one bar.
#define WARRIOR_SIG_LEVEL_HALF_SPAN 1.3
//--- Wingdings codes for the arrow half of the mark, and the direction token persisted in the
//--- .arrows sidecar - one number doing both jobs, as it originally did. The sidecar stores it, the
//--- line half recovers direction from its COLOUR (it carries no code), and the arrow half draws it.
#define WARRIOR_SIG_CODE_BUY 217
#define WARRIOR_SIG_CODE_SELL 218
//--- How far back either chart rebuild reaches: the AI members' "Show signals" rescan and the
//--- aggregate's historical filtered overlay.
#ifndef SIGNAL_RESCAN_LOOKBACK_BARS
#define SIGNAL_RESCAN_LOOKBACK_BARS 5000
#endif
//--- Panel "Hide signals" toggle (Warrior_EA.mq5). Read when creating an arrow so one drawn while the
//--- toggle is off is born hidden rather than flashing onto the chart until the next sweep.
extern bool g_signalsVisible;
//--- The arrow half's object name is the line's plus this suffix, so it stays inside SIG_ARROW_PREFIX
//--- and every prefix-scoped purge, sidecar scan and visibility sweep already reaches it unchanged.
#define WARRIOR_SIG_ARROW_SUFFIX "_a"
string WarriorSignalArrowName(const string lineName)
{
return lineName + WARRIOR_SIG_ARROW_SUFFIX;
}
//+------------------------------------------------------------------+
//| Removes a signal mark - BOTH halves. Every caller that used to |
//| ObjectDelete the line name must come through here, or the arrow |
//| outlives the line it belongs to and the chart accumulates marks |
//| for signals that were withdrawn. |
//+------------------------------------------------------------------+
void WarriorDeleteSignalMark(const string name)
{
ObjectDelete(0, name);
ObjectDelete(0, WarriorSignalArrowName(name));
}
//+------------------------------------------------------------------+
//| The one place a signal mark is actually created. Deliberately a |
//| free function rather than a method: four unrelated callers need |
//| it (a classic filter, the aggregate signal's vote layer, its |
//| historical overlay rebuild, and the AI members' own raw view) |
//| and only some of them are signal objects at all. |
//+------------------------------------------------------------------+
void WarriorPlotSignalLevel(const string name, const datetime t, const ENUM_TIMEFRAMES period,
const double price, const bool isBuy, const bool isTrade,
const string tooltip)
{
if(t <= 0 || !MathIsValidNumber(price) || price <= 0.0)
return;
int half = (int)(PeriodSeconds(period) * WARRIOR_SIG_LEVEL_HALF_SPAN);
if(half <= 0)
half = 60;
ObjectCreate(0, name, OBJ_TREND, 0, t - half, price, t + half, price);
//--- Re-applied every call, not just at creation: this doubles as the refresh path, and a mark
//--- whose price moved (a redraw at a corrected level) must move with it.
ObjectSetInteger(0, name, OBJPROP_TIME, 0, t - half);
ObjectSetDouble(0, name, OBJPROP_PRICE, 0, price);
ObjectSetInteger(0, name, OBJPROP_TIME, 1, t + half);
ObjectSetDouble(0, name, OBJPROP_PRICE, 1, price);
//--- A trend line rays to infinity by default - that would paint the whole chart.
ObjectSetInteger(0, name, OBJPROP_RAY_LEFT, false);
ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false);
ObjectSetInteger(0, name, OBJPROP_COLOR, isBuy ? WARRIOR_SIG_BUY_COLOR : WARRIOR_SIG_SELL_COLOR);
//--- Thicker on both layers for the same reason the span grew (2026-08-19): a 1px dotted dark
//--- line on a candle chart is invisible at any realistic zoom. The trade layer stays the
//--- heavier of the two so the ranking still reads at a glance.
ObjectSetInteger(0, name, OBJPROP_WIDTH, isTrade ? 3 : 2);
ObjectSetInteger(0, name, OBJPROP_STYLE, isTrade ? STYLE_SOLID : STYLE_DOT);
//--- Not selectable: these are readouts, and a chart carrying thousands of them becomes
//--- unusable if a stray drag can pick one up and move it.
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
ObjectSetInteger(0, name, OBJPROP_BACK, !isTrade); // opinions behind the candles, trades in front
ObjectSetInteger(0, name, OBJPROP_TIMEFRAMES, g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS);
ObjectSetString(0, name, OBJPROP_TOOLTIP, tooltip);
//--- THE FINDER HALF. Anchored to the candle's extreme rather than the trigger price so it
//--- clears the body at every zoom - the whole point is to be visible when the line is not.
string an = WarriorSignalArrowName(name);
int shift = iBarShift(_Symbol, period, t, true);
double anchorPrice = price;
if(shift >= 0)
anchorPrice = isBuy ? iLow(_Symbol, period, shift) : iHigh(_Symbol, period, shift);
if(!MathIsValidNumber(anchorPrice) || anchorPrice <= 0.0)
anchorPrice = price;
ObjectCreate(0, an, OBJ_ARROW, 0, t, anchorPrice);
ObjectSetInteger(0, an, OBJPROP_TIME, 0, t);
ObjectSetDouble(0, an, OBJPROP_PRICE, 0, anchorPrice);
ObjectSetInteger(0, an, OBJPROP_ARROWCODE, isBuy ? WARRIOR_SIG_CODE_BUY : WARRIOR_SIG_CODE_SELL);
//--- ANCHOR is what keeps the glyph OUTSIDE the candle: its top pinned to the low hangs it below,
//--- its bottom pinned to the high stands it above. Anchoring the centre would bury it in the wick.
ObjectSetInteger(0, an, OBJPROP_ANCHOR, isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM);
ObjectSetInteger(0, an, OBJPROP_COLOR, isBuy ? WARRIOR_SIG_BUY_COLOR : WARRIOR_SIG_SELL_COLOR);
ObjectSetInteger(0, an, OBJPROP_WIDTH, isTrade ? 2 : 1);
ObjectSetInteger(0, an, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, an, OBJPROP_HIDDEN, true);
ObjectSetInteger(0, an, OBJPROP_BACK, !isTrade);
ObjectSetInteger(0, an, OBJPROP_TIMEFRAMES, g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS);
ObjectSetString(0, an, OBJPROP_TOOLTIP, tooltip);
}
//--- THE VOTE READOUT's own object namespace.
#define VOTE_HUD_PREFIX "WarriorVoteHUD"
//--- Overlay declustering window, in bars - same default as the per-member arrows'
//--- m_signalClusterWindow. A root-level constant rather than a borrowed member because the root
//--- has no AI state and the two layers may legitimately diverge later.
#define OVERLAY_NMS_WINDOW 6
//--- INTELLIGENT trade direction - the measured drift verdict, written by the label-cache prebuild
//--- (Expert\AIBase\Labels.mqh, see the verdict block there for the statistics). BOTH until
//--- measured - the safe state, and the permanent state on classic-only charts, which never build a
//--- label cache.
TRADING_DIRECTION g_warriorDriftVerdict = BOTH;
bool g_warriorDriftMeasured = false;
//--- The one resolution point for the Trade direction input: INTELLIGENT defers to the measured
//--- verdict, everything else is what it always was. Every gate - live entry, reconstruction,
//--- HUD verdict - resolves through here so they cannot drift apart.
TRADING_DIRECTION WarriorEffectiveDirection(void)
{
return (tradingdirection == DIRECTION_INTELLIGENT) ? g_warriorDriftVerdict : tradingdirection;
}
bool WarriorDirectionAllows(const bool isLong)
{
TRADING_DIRECTION d = WarriorEffectiveDirection();
return isLong ? (d != SHORT_ONLY) : (d != LONG_ONLY);
}
//--- META-LABELING GATE HOOK (2026-08-19, Meta_Labeling_Design.md S3). Non-NULL only when
//--- Use_MetaLabeling created a meta head this run (set in InitializeSignal, cleared at every re-
//--- init before signal creation).
class CExpertSignalCustom;
CExpertSignalCustom *g_warriorMetaGate = NULL;
//--- The symbol's own trading-session table, asked two questions (2026-08-19 user request:
//--- "everything will be dynamic and self adapting to DST"). Is `now` (server time) inside any
//--- trading session of its weekday?
bool WarriorMarketOpenNow(const string symbol, const datetime now)
{
MqlDateTime dt;
TimeToStruct(now, dt);
int secOfDay = dt.hour * 3600 + dt.min * 60 + dt.sec;
datetime from = 0, to = 0;
for(uint s = 0; SymbolInfoSessionTrade(symbol, (ENUM_DAY_OF_WEEK)dt.day_of_week, s, from, to); s++)
{
if(secOfDay >= (int)from && secOfDay < (int)to)
return true;
}
return false;
}
//--- The LAST session close of the given weekday, in seconds from that day's midnight (86400 on
//--- symbols that trade to midnight). -1 = no trading that day.
int WarriorMarketCloseSeconds(const string symbol, const int dayOfWeek)
{
datetime from = 0, to = 0;
int lastTo = -1;
for(uint s = 0; SymbolInfoSessionTrade(symbol, (ENUM_DAY_OF_WEEK)dayOfWeek, s, from, to); s++)
lastTo = (int)to;
return lastTo;
}
//
#define MAX_TABLE_ROWS 1000 // default row cap before the oldest entry is pruned; the live
// value comes from the DB_MaxRowsPerTable input via
// MaxTableRows() - raised for meta-label corpus builds
#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
//--- Hard floor on SL distance from entry, as an ATR multiple. Pure sanity net: the broker's own
//--- SYMBOL_TRADE_STOPS_LEVEL is enforced separately and precisely by TCAdjustStops() further down.
#define MIN_SL_ATR_MULTIPLIER 0.5
//--- 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).
#define SL_INTELLIGENT_MODE (-1)
#define TP_INTELLIGENT_MODE (-1)
//--- The SL_PREV_SWING / TP_PREV_SWING sentinels (-101) were REMOVED 2026-07-31 along with every
//--- other swing anchor on SL and TP - see STOP_LOSS_MODE in Enumerations\InputEnums.mqh.
#define SL_INTELLIGENT_BASE_MULT 3.0
#define TP_INTELLIGENT_BASE_RR 2.5
#define AI_SL_TIGHTEN_FACTOR 0.3
#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.
#define ENTRY_INTELLIGENT_MODE (-100)
#define ENTRY_PREV_SWING_MODE (-101)
#define ENTRY_INTELLIGENT_BASE_MULT 2.0
//
class CExpertSignalCustom : public CExpertSignal
{
private:
void DeleteOldestEntry(string tableName);
//--- (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.)
void UpdateTradeRecordInDatabase(string tableName, TradeRecord &tradeRecord);
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);
bool ShouldTraceTradeRejections(void) const;
//--- 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);
void BufferNewTickSignal(string filterID, string pattern, string bias, const MqlDateTime& brokerTime, double entryPrice, double netVote);
string PatternName(int patternIndex) { return "Pattern_" + IntegerToString(patternIndex); }
SignalInfo signalBuffer[];
protected:
//--- protected (not private): CExpertSignalAIBase's pattern-database backfill (Expert\AIBase\
//--- OnlineLearning.mqh) calls both directly, so the training-time path can journal into the exact
//--- same tables/rows the live per-tick path (BufferNewTickSignal above) writes to.
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, double netVote);
string PatternTableName(string filterID, string pattern, string direction);
bool m_prohibition_signal;
bool m_useDatabase;
CiATR m_ATR; // ATR indicator
string m_id;
//--- m_active_pattern/m_active_direction are the SCRATCH slots the signal classes' Long/Short
//--- ladders write into (last-writer-wins WITHIN one ladder is intended - it is the grading).
//--- The DB journaling reads ONLY the per-side slots.
string m_active_pattern;
string m_active_direction;
string m_active_pattern_long; // long ladder's match on the last evaluation, or "NULL"
string m_active_pattern_short; // short ladder's match on the last evaluation, or "NULL"
//--- This filter's own net vote, LongCondition() - ShortCondition(), in pattern-weight units
//--- before m_weight scaling. Same sign as m_lastFiredDirection; journaled into the netVote
//--- column as DATA, never used as a journaling filter - see the per-side journaling comment in
//--- Direction().
double m_lastNetVote;
//--- The two ladder results behind m_lastNetVote, kept apart from it because the net alone
//--- cannot answer "at what weight".
int m_lastLongWeight;
int m_lastShortWeight;
//--- HISTORICAL FILTERED-OVERLAY sweep state (see AdvanceFilteredOverlay).
bool m_overlayPending;
int m_overlayIndex; // next bar index to process, walking newest -> oldest
int m_overlayStopIndex; // lowest (most recent) series index the sweep reaches
//--- Bar time at which the EA took over drawing arrows itself. The sweep RECONSTRUCTS what the
//--- vote would have been; forward of this the arrows are the real decision, placed by
//--- CheckOpenPosition after the order parameters validated.
datetime m_overlayLiveCutoff;
//--- Per-sweep census, so a blank filtered view can state its own cause - see the report at the
//--- end of AdvanceFilteredOverlay().
int m_overlaySweptBars;
int m_overlayVotedBars;
int m_overlayDrawn;
//--- Census-log change latch (2026-08-19): the sweep completes ~once a minute and its census
//--- line printed every time - ~560 near-identical lines/day.
int m_overlayLastLogDrawn;
double m_overlayLastLogBest;
int m_overlaySkippedLogs;
double m_overlayBestNet;
int m_overlayVotedBuy;
int m_overlayVotedSell;
//--- Sweep-scoped NMS state (see the decluster block in AdvanceFilteredOverlay). Members rather
//--- than locals because the sweep is chunked across timer slices; reset at every arm.
int m_overlayNmsLastBuyIdx;
int m_overlayNmsLastSellIdx;
int m_overlayNmsKeptIdx;
bool m_overlayNmsKeptBuy;
double m_overlayNmsKeptNet;
//--- Session peak |vote|, for the readout. The single most useful number for choosing
//--- Signal_ThresholdOpen: a threshold above the peak can never fire, and until this was on screen the
//--- only way to learn that was to wait an era and read the gate line.
double m_votePeak;
//--- How many per-member HUD lines are currently on the chart, so a shrink (member disabled,
//--- filters rebuilt) deletes the orphans instead of leaving a frozen line from a model that
//--- no longer exists - the exact stale-display failure the snapshot rule exists to prevent.
int m_hudMemberLines;
//--- Live voter count from the most recent Direction() call. RefreshVoteReadout() keys on it: a
//--- bar with real voters keeps its display; only a voterless bar is repainted prospectively.
int m_lastLiveVoters;
int m_maxTableRows; // per-table row cap, from the DB_MaxRowsPerTable input
int m_pattern_count;
double m_entry_multiplier; // Configurable multiple for ATR entry adjustment
int m_periods; // ATR periods
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
int m_confidence_source; // CONFIDENCE_SOURCE underlying int (0=AI, 1=DB, 2=Blended)
//--- HOLD-TO-BARRIER exit policy (2026-08-15, fractal-target fidelity). The deploy gate
//--- certifies a win rate measured on hold-to-resolution outcomes: entry at the signal bar, then
//--- the measured SL or TP decides.
bool m_holdToBarrier;
double m_dbConfidence; // last average normalized DB win-rate across active filters
//--- Direction()'s per-second aggregation state. The window key is a full timestamp (broker
//--- clock since 2026-08-19), NOT MqlDateTime.sec.
datetime m_directionCurrentSecond;
double m_directionAggregatedResult;
int m_directionCount;
double m_directionLastResult;
int m_lastFiredDirection; // +1 Buy / -1 Sell / 0 none - THIS filter's own latest vote,
// set in Direction() before children are added in. Unlike
// GetActivePatternLong()/Short(), never consumed/reset by
// a read - a pure peek, safe for a parent to poll every tick.
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 SLMode(int value) { m_sl_mode = value; }
void TPMode(int value) { m_tp_mode = value; }
void ConfidenceSource(int value) { m_confidence_source = value; }
void HoldToBarrier(bool value) { m_holdToBarrier = value; }
bool HoldToBarrier(void) const { return m_holdToBarrier; }
//--- HISTORICAL EVALUATION SHIFT (meta-labeling candidate sweep). Non-zero only inside
//--- CSignalMETA's corpus sweep; 0 = normal live behaviour (base rule: every_tick ? 0 : 1).
int m_evalShift;
void EvalShift(const int shift) { m_evalShift = shift; }
//--- CONFIGURED EVALUATION BAR (Classic_Shift input, classic votes only). The sweep above still
//--- wins when it is active.
int m_shift;
void Shift(const int shift) { m_shift = (shift < 0 ? -1 : shift); }
virtual int StartIndex(void)
{
if(m_evalShift > 0)
return m_evalShift;
return (m_shift >= 0 ? m_shift : (m_every_tick ? 0 : 1));
}
//--- Deep-history readiness for the sweep: the price series and each signal's own indicator
//--- buffers default to a shallow depth, so reads at bar 40,000 would fail. Overridden per signal
//--- class to also resize its indicator; the base handles the shared price series.
virtual bool SweepPrepare(const int bars)
{
bool ok = true;
if(CheckPointer(m_open) != POINTER_INVALID)
{
ok = m_open.BufferResize(bars) && ok;
m_open.Refresh(-1);
}
if(CheckPointer(m_high) != POINTER_INVALID)
{
ok = m_high.BufferResize(bars) && ok;
m_high.Refresh(-1);
}
if(CheckPointer(m_low) != POINTER_INVALID)
{
ok = m_low.BufferResize(bars) && ok;
m_low.Refresh(-1);
}
if(CheckPointer(m_close) != POINTER_INVALID)
{
ok = m_close.BufferResize(bars) && ok;
m_close.Refresh(-1);
}
return ok;
}
// 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; }
//--- 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).
double LiveSignedConfidence(void);
// Combines AIConfidence()/m_dbConfidence per m_confidence_source into a single 0..1
// magnitude, used to scale SL/TP and (Intelligent MM) lot size.
double EffectiveConfidence(void);
virtual void ApplyPatternWeight(int patternNumber, int weight) {};
void ID(string id) { m_id = id; }
virtual string GetFilterID(void) { return m_id; };
//--- Is this filter one of the neural nets? Overridden true by CExpertSignalAIBase. A virtual
//--- rather than a GetFilterID() string comparison because the ids are FOLDER names that outlive
//--- display renames (SignalHYBRID's "ConvLSTM"/"HYB" pair), so a name test would silently start
//--- returning the wrong answer the next time a model is renamed.
virtual bool IsAIFilter(void) const { return false; }
//--- CONTROL-PANEL SEAM. The panel used to drive training through g_aiSignals[] in
//--- Warrior_EA.mq5 - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had
//--- already dropped a member on the floor once (609be10).
virtual bool OnSignalCommand(const ENUM_SIGNAL_COMMAND cmd) { return false; }
virtual bool HasSignalTrait(const ENUM_SIGNAL_TRAIT trait) { return false; }
//--- Whole-tree walks: this signal plus every filter, recursively.
int DispatchSignalCommand(const ENUM_SIGNAL_COMMAND cmd);
int CountSignalTrait(const ENUM_SIGNAL_TRAIT trait);
//--- META-LABELING GATE SEAM (S3, 2026-08-19). Overridden only by CSignalMETA; the base is a no-
//--- op so a chart without a meta head pays nothing. No default argument on purpose: every
//--- caller states its bar.
virtual int LiveMetaGate(const bool isLong, const double netVote, double &pWin,
double &bePct, const int barIdx)
{ pWin = -1.0; bePct = -1.0; return 0; }
//--- Does this filter derive its own pattern weights, making the signal DB's ranking
//--- inapplicable to it?
virtual bool SelfRanked(void) const { return false; }
//--- The weight this filter contributes to the vote's DENOMINATOR - its say in the consensus -
//--- independent of whether it votes on this particular bar.
virtual double VoteCapableWeight(void) { return (GetPatternCount() > 0) ? m_weight : 0.0; }
//--- AI filters only: this model's cached decision for bar `idx`, already converted to the signed
//--- vote it would have cast. False when the bar was never scored (outside the scan, or a feature
//--- window failure), which is NOT the same as an abstention and must not be counted as one.
virtual bool CachedVoteAt(const int idx, double &signedVote) { signedVote = 0.0; return false; }
//--- Same question asked of the member's ERA-END SNAPSHOT instead of its live cache. The live
//--- cache is wiped to sentinel at every era start, so anything reading it is blind for most of
//--- every era - the snapshot is copied at pass-3 completion and survives until the next one.
virtual bool SnapshotVoteAt(const int idx, double &signedVote) { signedVote = 0.0; return false; }
//--- One HUD line describing this member's CURRENT raw opinion - the output neurons, the
//--- decision they resolve to, its weighted vote, era and training error. Empty string = no
//--- line; only AI members override.
virtual string DisplayHudLine(void) { return ""; }
//--- What this filter WOULD vote right now if it were allowed to - i.e. its current decision put
//--- through the same tier/weight arithmetic, but WITHOUT the readiness gate that stops a model
//--- voting before it is deployed.
virtual bool ProspectiveVote(double &signedVote, double &weight)
{ signedVote = 0.0; weight = 0.0; return false; }
//--- Snapshot/restore of everything a Direction() call writes that a LATER call reads. That is a
//--- corrupted row in the very table the pattern win rates (and now the vote weights) are
//--- computed from.
void SaveVoteState(string &pl, string &ps, double &nv, int &lw, int &sw, int &fd)
{
pl = m_active_pattern_long; ps = m_active_pattern_short; nv = m_lastNetVote;
lw = m_lastLongWeight; sw = m_lastShortWeight; fd = m_lastFiredDirection;
}
void RestoreVoteState(const string pl, const string ps, const double nv,
const int lw, const int sw, const int fd)
{
m_active_pattern_long = pl; m_active_pattern_short = ps; m_lastNetVote = nv;
m_lastLongWeight = lw; m_lastShortWeight = sw; m_lastFiredDirection = fd;
}
//--- Chunked historical rebuild of the FILTERED view - see the definition for the whole rationale.
bool AdvanceFilteredOverlay(const int barBudget);
void StartFilteredOverlay(void);
bool FilteredOverlayPending(void) const { return m_overlayPending; }
//--- One-line on-chart readout of the vote that is actually being tested against Signal_ThresholdOpen.
void UpdateVoteReadout(const double vote, const int voters, const int neutrals, const bool prospective);
//--- Timer-driven repaint of the readout - see the definition for the cadence bug it fixes.
void RefreshVoteReadout(void);
//--- THIS filter's own arrow namespace.
string FilterArrowPrefix(void) { return SIG_ARROW_PREFIX + m_id + "_"; }
//--- RAW VIEW: draw this filter's own vote at bar `idx`, named and tooltipped so it identifies
//--- itself on a chart carrying several.
void DrawRawFilterArrow(const int idx, const string pattern, const bool isBuy,
const int weight)
{
datetime t = iTime(m_symbol.Name(), m_period, idx);
//--- THE TRIGGER PRICE: this bar's close, which is where a market order fires and exactly the
//--- entry the triple-barrier label assumes (see TripleBarrierLabel). The spread the label
//--- charges is smaller than a chart pixel at normal zoom, so it is priced but not drawn.
double price = iClose(m_symbol.Name(), m_period, idx);
WarriorPlotSignalLevel(FilterArrowPrefix() + TimeToString(t), t, (ENUM_TIMEFRAMES)m_period, price,
isBuy, false,
StringFormat("%s %s %s (weight %d, module %.2f)", m_id, (isBuy ? "Buy" : "Sell"),
pattern, weight, m_weight));
}
//--- Remove this filter's arrow at bar `idx` - the counterpart to the draw above, for a bar whose
//--- vote was withdrawn (a rejected setup, or a redraw that no longer fires there).
void EraseRawFilterArrow(const int idx)
{
datetime t = iTime(m_symbol.Name(), m_period, idx);
if(t > 0)
WarriorDeleteSignalMark(FilterArrowPrefix() + TimeToString(t));
}
//--- FILTERED VIEW: the combined vote, drawn by the AGGREGATE signal and belonging to no filter.
//--- Bigger and in its own colours precisely so it does not read as "one more model's opinion" -
//--- it is a different kind of statement from the raw arrows and the two must never be confused
//--- on a chart that shows either.
void DrawVoteArrow(const int idx, const bool isBuy, const double vote,
const double sl, const double tp)
{
datetime t = iTime(m_symbol.Name(), m_period, idx);
//--- The trigger price - see DrawRawFilterArrow's note. This is the level the order goes on at.
double price = iClose(m_symbol.Name(), m_period, idx);
WarriorPlotSignalLevel(SIG_VOTE_PREFIX + TimeToString(t), t, (ENUM_TIMEFRAMES)m_period, price,
isBuy, true,
StringFormat("TRADE %s @ %s | vote %.1f >= %.1f | SL %s TP %s",
(isBuy ? "BUY" : "SELL"),
DoubleToString(price, m_symbol.Digits()), vote, m_threshold_open,
DoubleToString(sl, m_symbol.Digits()),
DoubleToString(tp, m_symbol.Digits())));
}
void EraseVoteArrow(const int idx)
{
datetime t = iTime(m_symbol.Name(), m_period, idx);
if(t > 0)
WarriorDeleteSignalMark(SIG_VOTE_PREFIX + TimeToString(t));
}
//--- Consuming reads (reset to "NULL" on read), one slot per side - the single-label
//--- GetActivePattern()/GetActiveDirection() pair they replace let the later-running short ladder
//--- steal the long ladder's label (see Direction()'s per-side journaling comment).
string GetActivePatternLong(void);
string GetActivePatternShort(void);
//--- NON-consuming peeks at the same two slots. Same relationship to GetActivePattern*() as
//--- m_lastFiredDirection has to those: a pure look, safe to call without stealing the value
//--- from the journaling path that must still receive it.
string PeekActivePatternLong(void) { return m_active_pattern_long; }
string PeekActivePatternShort(void) { return m_active_pattern_short; }
double LastNetVote(void) { return m_lastNetVote; }
int LastLongWeight(void) { return m_lastLongWeight; }
int LastShortWeight(void) { return m_lastShortWeight; }
//--- Read access to CExpertSignal's m_weight, which the standard library exposes only as a
//--- SETTER. Named ModuleWeight() rather than Weight() so it cannot be mistaken for (or
//--- accidentally overload) the library's setter.
double ModuleWeight(void) const { return m_weight; }
virtual int GetPatternCount(void) { return m_pattern_count; };
virtual double Direction(void) override;
//--- Vote lifecycle hooks, for filters whose LongCondition()/ShortCondition() consume one-shot
//--- state when they fire. Base = no-op.
virtual void BeginVote(void) {}
virtual void RevokeVote(void) {}
bool UpdateSignalsWeights(void);
//--- priorWeight 0 = raw maximum-likelihood ratio (the pre-2026-08-16 behaviour); >0 shrinks the
//--- estimate toward priorPct by that many pseudo-trades. See the definition for why.
int WinRateFromCounts(const int wins, const int losses, const double priorPct = -1.0,
const int priorWeight = 0);
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; };
void MaxTableRows(int value) { m_maxTableRows = MathMax(1, 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_active_pattern_long("NULL"),
m_active_pattern_short("NULL"),
m_lastNetVote(0.0),
m_lastLongWeight(0),
m_lastShortWeight(0),
m_overlayPending(false),
m_overlayIndex(0),
m_overlayStopIndex(0),
m_overlayLiveCutoff(0),
m_overlaySweptBars(0),
m_overlayVotedBars(0),
m_overlayDrawn(0),
m_overlayBestNet(0.0),
m_overlayLastLogDrawn(-1),
m_overlayLastLogBest(0.0),
m_overlaySkippedLogs(0),
m_overlayVotedBuy(0),
m_overlayVotedSell(0),
m_overlayNmsLastBuyIdx(-1),
m_overlayNmsLastSellIdx(-1),
m_overlayNmsKeptIdx(-1),
m_overlayNmsKeptBuy(false),
m_overlayNmsKeptNet(0.0),
m_votePeak(0.0),
m_hudMemberLines(0),
m_lastLiveVoters(0),
m_evalShift(0),
m_shift(-1),
m_maxTableRows(MAX_TABLE_ROWS),
m_pattern_count(0),
m_entry_multiplier(0),
m_prohibition_signal(false),
m_periods(14),
m_useDatabase(false),
m_sl_mode(3), // SL_ATR_x3
m_tp_mode(6), // TP_ATR_x6
m_confidence_source(0),
m_holdToBarrier(false),
m_dbConfidence(0.0),
m_directionCurrentSecond(0),
m_directionAggregatedResult(0.0),
m_directionCount(0),
m_directionLastResult(0.0),
m_lastFiredDirection(0)
{
}
//+------------------------------------------------------------------+
//| Combine AI/DB confidence per the configured Confidence_Source |
//+------------------------------------------------------------------+
double CExpertSignalCustom::LiveSignedConfidence(void)
{
double own = SignedAIConfidence();
if(own != 0.0)
return own;
//--- THE ORCHESTRATOR COMBINES; the members only publish.
g_LiveAISignedConfidence = AggregateAIVotes();
return g_LiveAISignedConfidence;
}
double CExpertSignalCustom::EffectiveConfidence(void)
{
g_AISignedConfidence = LiveSignedConfidence();
g_DBConfidence = m_dbConfidence;
return CombinedConfidence(m_confidence_source);
}
//+------------------------------------------------------------------+
//| Destructor |
//+------------------------------------------------------------------+
CExpertSignalCustom::~CExpertSignalCustom(void)
{
ArrayFree(signalBuffer);
}
//+------------------------------------------------------------------+
//| Tester-only trade rejection tracing |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::ShouldTraceTradeRejections(void) const
{
return VerboseMode;
}
void TraceSignalRejection(const string key, const string message)
{
if(!VerboseMode)
return;
TCLog("signal-reject:" + key, message);
}
//+------------------------------------------------------------------+
//| 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;
}
//+------------------------------------------------------------------+
//| 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. |
//+------------------------------------------------------------------+
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);
}
//+------------------------------------------------------------------+
//| 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(!MathIsValidNumber(atr) || atr <= 0.0)
return false; // ATR must be positive
if(!m_symbol.Name(_Symbol))
return false; // Symbol information must be accessible
//--- 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))
{
TraceSignalRejection("openparams-symbol:" + m_symbol.Name(),
__FUNCTION__ + ": rejected - " + tc_reason);
return false;
}
int lookback_period = m_periods;
//--- 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))
{
TraceSignalRejection("openparams-history:" + m_symbol.Name(),
__FUNCTION__ + ": rejected - " + tc_reason);
return false;
}
double base_price = (m_base_price == 0.0) ? (isLong ? m_symbol.Ask() : m_symbol.Bid()) : m_base_price;
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);
//--- Whether the swing prices are actually USED by this configuration. Since 2026-07-31 only
//--- ENTRY_PREV_SWING consumes them - SL and TP are both entry-anchored ATR multiples now. Kept
//--- as guards rather than deleted because a bad swing must still never reach an entry price.
bool needSwings = ((int)m_entry_multiplier == ENTRY_PREV_SWING_MODE);
if(needSwings && (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.
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;
}
//--- Index can legitimately be -1 here when !needSwings (the guard above no longer rejects for
//--- it), and iLow/iHigh with a negative index is undefined - so never call it in that case.
double lowest_low = (lowest_index >= 0) ? iLow(m_symbol.Name(), m_period, lowest_index) : 0.0;
double highest_high = (highest_index >= 0) ? iHigh(m_symbol.Name(), m_period, highest_index) : 0.0;
if(needSwings && (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(needSwings && (!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(needSwings && (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));
return false;
}
// 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.
double confidence = EffectiveConfidence();
if(!MathIsValidNumber(confidence))
confidence = 0.0;
//--- --- Entry price. Offsets are measured from the CURRENT price (base_price = bid/ask), except
//--- ENTRY_PREV_SWING which anchors to the recent swing.
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)
{
// 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));
}
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 ENTRY-anchored, a straight ATR multiple below (long) / above (short)
//--- the entry price. SL_ATR_* use that multiple directly; SL_INTELLIGENT starts at
//--- SL_INTELLIGENT_BASE_MULT and tightens as confidence rises.
//--- MEASURED GEOMETRY OVERRIDE (2026-08-09). A trade with any other geometry is a different
//--- bet, one the gate never graded - the model was being graded on one game and paid on
//--- another.
bool useDerivedGeometry = (g_DerivedSlAtrMult > 0.0 && g_DerivedTpAtrMult > 0.0);
double slMultiplier;
if(useDerivedGeometry)
slMultiplier = g_DerivedSlAtrMult;
else
if(m_sl_mode == SL_INTELLIGENT_MODE)
slMultiplier = SL_INTELLIGENT_BASE_MULT * (1.0 - AI_SL_TIGHTEN_FACTOR * confidence);
else
slMultiplier = (double)m_sl_mode;
sl = isLong ? m_symbol.NormalizePrice(price - slMultiplier * atr)
: m_symbol.NormalizePrice(price + slMultiplier * atr);
//--- Enforce a hard minimum SL distance from entry (broker stop-level / sanity floor).
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. Min RR (below) only rejects,
//--- never reshapes either.
if(useDerivedGeometry)
{
//--- ATR-anchored like the label, NOT risk-relative: the label measures "reach tp before sl" as
//--- two independent ATR distances from the entry, so the live target must be the same distance -
//--- tying it to the (possibly floor-widened) realised risk would silently reshape the certified
//--- geometry on exactly the trades whose stop got adjusted.
tp = isLong ? m_symbol.NormalizePrice(price + g_DerivedTpAtrMult * atr)
: m_symbol.NormalizePrice(price - g_DerivedTpAtrMult * atr);
}
else
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);
}
// 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;
}
//--- --- 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.
ENUM_ORDER_TYPE order_type = ResolveOrderType(isLong, price);
string stops_note;
if(!TCAdjustStops(m_symbol.Name(), order_type, price, sl, tp, stops_note))
{
TraceSignalRejection("openparams-stops:" + m_symbol.Name(), __FUNCTION__ + ": rejected - " + stops_note);
return false;
}
if(stops_note != "")
TraceSignalRejection("openparams-stops-adj:" + m_symbol.Name(), __FUNCTION__ + ": " + stops_note);
// 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))
{
TraceSignalRejection("openparams-stops-final:" + m_symbol.Name(), __FUNCTION__ + ": rejected - " + stops_note);
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))
{
TraceSignalRejection("openparams-pending:" + m_symbol.Name(), __FUNCTION__ + ": rejected - " + stops_note);
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))
{
TraceSignalRejection("openparams-orderlimit", __FUNCTION__ + ": rejected - " + stops_note);
return false;
}
//--- REWARD:RISK IS MEASURED AND PUBLISHED, NOT ENFORCED (2026-08-09). The minimum-ratio rejection
//--- that stood here is gone with the Min_Risk_Reward_Ratio input - see Variables\Inputs.mqh.
double reward = fabs(tp - price);
// Still computed and still bridged to Money\MoneyIntelligent.mqh's Kelly-criterion sizing - the
// ratio remains a genuine INPUT to how big the position should be, which is the use that was
// always sound. Only the veto is gone.
g_TradeRewardRiskRatio = (risk > 0.0) ? reward / risk : 0.0;
// 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)
{
//--- Hold-to-barrier: no vote-driven exit of any kind - see m_holdToBarrier's declaration comment.
//--- The base price is still zeroed, exactly as the normal path below does on every call.
if(m_holdToBarrier)
{
m_base_price = 0.0;
return false;
}
bool result = false;
//--- check of exceeding the threshold value, adjusted for long/short
double directionMultiplier = isLong ? -1 : 1;
//--- ONE EXIT AUTHORITY, and under an AI certificate it is the BARRIER, not a vote.
bool aiCertificateGoverns = (g_DerivedSlAtrMult > 0.0 && g_DerivedTpAtrMult > 0.0);
// Allowing position closing without checking the prohibition signal.
if(!aiCertificateGoverns && directionMultiplier * m_direction >= m_threshold_close)
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)
{
if(ShouldTraceTradeRejections())
TraceSignalRejection("open-prohibition",
StringFormat("%s: open %s rejected - a child filter vetoed the tick (prohibition signal).",
__FUNCTION__, isLong ? "long" : "short"));
return false;
}
//--- MARKET-HOURS GATE (2026-08-19). Entries only - exits, SL/TP and the scheduled close-all stay
//--- unguarded on purpose: closing risk must never be blocked by a session boundary.
if(!WarriorMarketOpenNow(m_symbol.Name(), TimeCurrent()))
{
if(ShouldTraceTradeRejections())
TraceSignalRejection("open-market-closed",
StringFormat("%s: open %s rejected - outside the symbol's trading sessions.",
__FUNCTION__, isLong ? "long" : "short"));
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;
//--- META-LABELING GATE (2026-08-19, user design: the meta head integrated into the voting
//--- decision pipeline). Entries only; exits, SL/TP and the scheduled close-all never consult
//--- it (closing risk must never be blocked).
if(g_warriorMetaGate != NULL)
{
double mgP = -1.0, mgBe = -1.0;
if(g_warriorMetaGate.LiveMetaGate(isLong, m_direction, mgP, mgBe, 1) < 0)
{
if(ShouldTraceTradeRejections())
TraceSignalRejection("open-meta-veto",
StringFormat("%s: open %s rejected by the meta gate - P(win) %.1f%% below the"
" cost-adjusted break-even %.1f%% (vote %.1f).",
__FUNCTION__, isLong ? "long" : "short",
100.0 * mgP, mgBe, m_direction));
return false;
}
}
//--- try to get the levels of opening, differentiating based on isLong
if(!(isLong ? OpenLongParams(price, sl, tp, expiration) : OpenShortParams(price, sl, tp, expiration)))
{
//--- FILTERED VIEW, and the reason this arrow is drawn HERE and not where the threshold is
//--- cleared: passing the vote is not the same as trading. A setup can clear
//--- Signal_ThresholdOpen and still never reach the broker - invalid SL/TP, stops-level, ATR
//--- warm-up, unsynced swing history - and every one of those failures lands in this branch.
EraseVoteArrow(StartIndex());
// 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();
if(ShouldTraceTradeRejections())
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"));
result = false;
}
//--- SURVIVED EVERYTHING: the vote cleared the threshold, no filter vetoed the tick, and the
//--- order parameters validated. THIS is the bar the EA would have placed an order on, so this
//--- is the only place the filtered view may mark. One arrow == one entry the bot would take.
else
if(!DrawUnfilteredSignals)
DrawVoteArrow(StartIndex(), isLong, directionMultiplier * m_direction, sl, tp);
}
else if(ShouldTraceTradeRejections())
{
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));
}
//--- 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 (INTELLIGENT resolves to the
// measured drift verdict - see WarriorEffectiveDirection)
if(WarriorDirectionAllows(true))
{
return CheckOpenPosition(true, price, sl, tp, expiration);
}
// The effective policy blocks longs
if(ShouldTraceTradeRejections())
TraceSignalRejection("open-long-direction-block",
StringFormat("%s: open long rejected - %s blocks long entries.", __FUNCTION__,
(tradingdirection == DIRECTION_INTELLIGENT) ? "the measured drift verdict (Intelligent)" : "strategy direction"));
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 (INTELLIGENT resolves to the
// measured drift verdict - see WarriorEffectiveDirection)
if(WarriorDirectionAllows(false))
{
return CheckOpenPosition(false, price, sl, tp, expiration);
}
// The effective policy blocks shorts
if(ShouldTraceTradeRejections())
TraceSignalRejection("open-short-direction-block",
StringFormat("%s: open short rejected - %s blocks short entries.", __FUNCTION__,
(tradingdirection == DIRECTION_INTELLIGENT) ? "the measured drift verdict (Intelligent)" : "strategy direction"));
return false;
}
//+------------------------------------------------------------------+
//| Return the long ladder's matched pattern (consuming read) |
//+------------------------------------------------------------------+
string CExpertSignalCustom::GetActivePatternLong(void)
{
string ret = m_active_pattern_long;
m_active_pattern_long = "NULL";
return ret;
}
//+------------------------------------------------------------------+
//| Return the short ladder's matched pattern (consuming read) |
//+------------------------------------------------------------------+
string CExpertSignalCustom::GetActivePatternShort(void)
{
string ret = m_active_pattern_short;
m_active_pattern_short = "NULL";
return ret;
}
//+------------------------------------------------------------------+
//| Detecting the "weighted" direction |
//+------------------------------------------------------------------+
double CExpertSignalCustom::Direction(void)
{
//--- BROKER TIME (2026-08-19, dbVersion 4.0): this one clock stamps every journaled DB row (the
//--- SignalInfo build below) and keys the once-per-second vote window. One clock, the broker's,
//--- everywhere.
MqlDateTime brokerTime;
datetime nowBroker = TimeCurrent(brokerTime); // 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.
if(nowBroker != m_directionCurrentSecond)
{
m_directionAggregatedResult = 0.0;
m_directionCount = 0;
m_directionCurrentSecond = nowBroker; // Update the current second
}
m_prohibition_signal = false;
BeginVote(); // snapshot any one-shot vote state, so a discarded vote can be rolled back - see BeginVote()
//--- Evaluate the two ladders separately and snapshot each one's matched pattern into its own side
//--- slot, keyed on the ladder having SET a label rather than on its returned weight - a pattern
//--- ranked down to weight 0 by UpdateSignalsWeights() still fired, and gating the snapshot on
//--- weight would freeze a 0%-win-rate pattern out of the very table that could ever raise it back.
//--- The scratch is cleared before each call so a stale label from a previous bar (or the other
//--- ladder) can never be attributed to a ladder that matched nothing this bar.
m_active_pattern = "NULL";
int longResult = LongCondition();
m_active_pattern_long = m_active_pattern;
m_active_pattern = "NULL";
int shortResult = ShortCondition();
m_active_pattern_short = m_active_pattern;
m_lastNetVote = longResult - shortResult;
m_lastLongWeight = longResult;
m_lastShortWeight = shortResult;
double result = m_weight * (longResult - shortResult);
//--- 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);
int number = (result == 0.0) ? 0 : 1;
//--- The weighted mean's DIVISOR, seeded with this signal's own module weight on exactly the
//--- same condition `number` is seeded - an abstention contributes to neither sum. It matters
//--- for a filter that has children of its own.
double weightSum = (result == 0.0) ? 0.0 : m_weight;
int total = m_filters.Total();
PrintVerbose("Starting direction calculation with total filters: " + IntegerToString(total));
//--- 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.
double directions[];
ArrayResize(directions, total);
bool aborted = false;
for(int i = 0; i < total; i++)
{
long mask = ((long)1) << i;
if((m_ignore & mask) != 0)
{
directions[i] = EMPTY_VALUE;
continue;
}
CExpertSignalCustom *filter = m_filters.At(i);
if(filter == NULL)
{
Print("Error: Filter at index " + IntegerToString(i) + " is NULL");
directions[i] = EMPTY_VALUE;
continue;
}
string filterID = filter.GetFilterID();
//--- Per-side pattern journaling: each ladder that MATCHED on this filter's last evaluation
//--- writes its own row, labelled by its own side, with the filter's net vote stored as data
//--- (netVote column) rather than used as a drop filter.
string patternLong = filter.GetActivePatternLong();
string patternShort = filter.GetActivePatternShort();
if(filterID != "NULL" && m_useDatabase)
{
double filterNetVote = filter.LastNetVote();
if(patternLong != "NULL")
BufferNewTickSignal(filterID, patternLong, "Buy", brokerTime, m_symbol.Ask(), filterNetVote);
if(patternShort != "NULL")
BufferNewTickSignal(filterID, patternShort, "Sell", brokerTime, m_symbol.Bid(), filterNetVote);
}
double direction = filter.Direction();
//--- RAW VIEW, classic filters only, and it must sit AFTER the Direction() call above rather
//--- than beside the journaling block.
if(DrawUnfilteredSignals && !filter.IsAIFilter())
{
int rawIdx = filter.StartIndex();
string freshLong = filter.PeekActivePatternLong();
string freshShort = filter.PeekActivePatternShort();
if(freshLong != "NULL")
filter.DrawRawFilterArrow(rawIdx, freshLong, true, filter.LastLongWeight());
else
if(freshShort != "NULL")
filter.DrawRawFilterArrow(rawIdx, freshShort, false, filter.LastShortWeight());
else
filter.EraseRawFilterArrow(rawIdx);
}
if(direction == EMPTY_VALUE)
{
m_prohibition_signal = true;
directions[i] = EMPTY_VALUE;
continue;
}
// 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.");
result = 0;
number = 0;
aborted = true;
break;
}
directions[i] = direction;
}
//--- 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();
}
}
//--- Pass 2: sum each filter's cached contribution, and accumulate the CONSENSUS denominator.
if(!aborted)
{
for(int i = 0; i < total; i++)
{
double direction = directions[i];
if(direction == EMPTY_VALUE)
continue;
CExpertSignalCustom *filter = m_filters.At(i);
//--- The say this filter has, granted by CAPABILITY rather than by participation - see
//--- VoteCapableWeight(). Accumulated before the abstention skip on purpose: an abstainer
//--- dilutes, that is the whole point of consensus.
double capW = filter.VoteCapableWeight();
weightSum += capW;
if(direction == 0)
continue;
number++; // voters only - the display's "N voter(s)" and the fired/abstained distinction
long mask = ((long)1) << i;
double signedDir = ((m_invert & mask) != 0) ? -direction : direction;
result += signedDir;
}
}
//--- NORMALIZATION - the divisor is the CAPABLE weight (see pass 2), so the result reads as "win-
//--- rate estimate x fraction of the ensemble's trust that agrees, net".
if(!aborted && total > 0 && weightSum > 0.0)
result /= weightSum;
//--- 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.
m_directionAggregatedResult += result;
m_directionCount++;
m_directionLastResult = m_directionAggregatedResult / 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.");
}
//--- READOUT, aggregate only. Placed AFTER the range check so the label shows what the threshold
//--- is actually tested against, not a pre-clamp value.
if(total > 0)
{
//--- NOBODY VOTED - and by far the most common reason is that no model is DEPLOYED yet, not
//--- that they all abstained.
m_lastLiveVoters = number;
//--- neutrals = -1: the live pass does not track how many filters answered Neutral (they are
//--- skipped in pass 2 without a count), so the label shows the plain voter count here.
if(number > 0)
UpdateVoteReadout(m_directionLastResult, number, -1, false);
else
RefreshVoteReadout();
}
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& brokerTime, double entryPrice, double netVote)
{
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 = {brokerTime.year, brokerTime.mon, brokerTime.day, brokerTime.day_of_week, brokerTime.hour, brokerTime.min, tableName, pattern, bias, entryPrice, netVote};
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");
}
//--- Every question below is answered by a targeted SQL lookup returning one row or one number.
//--- Per-signal cost is now flat in table size.
int curCount = 0, oppCount = 0;
if(!dbm.FetchRecordCount(currentTableName, curCount))
{
Print("Failed to count current direction trades in: " + currentTableName);
return;
}
if(!dbm.FetchRecordCount(oppositeTableName, oppCount))
{
Print("Failed to count opposite direction trades in: " + oppositeTableName);
return;
}
if(curCount >= m_maxTableRows)
DeleteOldestEntry(currentTableName);
if(oppCount >= m_maxTableRows)
DeleteOldestEntry(oppositeTableName);
//--- Close the opposite direction's open trade, if any. Closing does NOT absorb the signal: the
//--- reversing signal still registers its own trade below (true stop-AND-reverse). State patterns
//--- escaped only by re-firing one bar later. The side that never registered also never got a win
//--- rate, so UpdateSignalsWeights() weighted the pattern from one side only.
string oppositeDirection = (signal.direction == "Buy") ? "Sell" : "Buy";
double oppEntry = 0.0;
bool oppOpen = false;
if(!dbm.FetchOpenTradeEntry(oppositeTableName, signal.pattern, oppositeDirection, oppEntry, oppOpen))
return;
if(oppOpen)
{
double profitLoss = (oppositeDirection == "Buy") ? (signal.entryPrice - oppEntry)
: (oppEntry - signal.entryPrice);
TradeRecord closeRec;
closeRec.pattern = signal.pattern;
closeRec.direction = oppositeDirection;
closeRec.exitPrice = signal.entryPrice;
closeRec.result = profitLoss >= 0 ? "Profit" : "Loss";
UpdateTradeRecordInDatabase(oppositeTableName, closeRec);
PrintVerbose("Closed opposite trade: " + oppositeTableName + ", Profit/Loss: " + DoubleToString(profitLoss));
}
//--- Duplicate / outdated / out-of-order guard: rows are inserted in chronological order, so the
//--- newest row (max ROWID) carries the table's latest timestamp; a signal at or before it is a
//--- duplicate or a replay and must not register.
long newestKey = 0;
bool hasRows = false;
if(!dbm.FetchNewestTimeKey(currentTableName, newestKey, hasRows))
return;
long sigKey = SignalTimeKey(signal.year, signal.month, signal.day, signal.hour, signal.minutes);
if(hasRows && newestKey >= sigKey)
{
PrintVerbose("Duplicate or outdated signal, not registering. Table: " + currentTableName);
return;
}
// One open trade per pattern+side at most
double curEntry = 0.0;
bool curOpen = false;
if(!dbm.FetchOpenTradeEntry(currentTableName, signal.pattern, signal.direction, curEntry, curOpen))
return;
if(curOpen)
{
PrintVerbose("Open trade found, not registering new trade. Table: " + currentTableName + ", Pattern: " + signal.pattern);
return;
}
// Register a new trade if no duplicates, outdated, or open trades were found above
RegisterSignal(signal.year, signal.month, signal.day, signal.DOW, signal.hour, signal.minutes,
currentTableName, signal.pattern, signal.direction, signal.entryPrice, 0.0, "NA", signal.netVote);
PrintVerbose("Registered new trade in table: " + currentTableName + ", Pattern: " + signal.pattern + ", Direction: " + signal.direction);
}
//+------------------------------------------------------------------+
//| yyyymmddhhmm as a number - the ordering key the targeted DB |
//| lookups compare on (matches the SQL expression they compute) |
//+------------------------------------------------------------------+
long SignalTimeKey(const int year, const int month, const int day, const int hour, const int minutes)
{
return ((((long)year * 100 + month) * 100 + day) * 100 + hour) * 100 + minutes;
}
//+------------------------------------------------------------------+
//| ONE LINE, TOP-RIGHT: the vote that is actually being tested. |
//+------------------------------------------------------------------+
void CExpertSignalCustom::UpdateVoteReadout(const double vote, const int voters, const int neutrals,
const bool prospective)
{
double mag = MathAbs(vote);
if(MathIsValidNumber(mag) && mag > m_votePeak)
m_votePeak = mag;
//--- The peak SHOWN is the larger of the live peak and the overlay census's strongest vote.
double peak = MathMax(m_votePeak, m_overlayBestNet);
//--- A PROSPECTIVE vote can never be a trade, however high it reads - the models are not
//--- deployed. Saying "-> TRADE" on a number that cannot place an order would be the exact
//--- overstatement this readout exists to prevent.
bool fires = (mag >= m_threshold_open) && (voters > 0) && !prospective &&
(vote == 0.0 || WarriorDirectionAllows(vote > 0.0));
//--- THE HEADLINE WORD IS THE DECISION, NOT THE LEAN (user request 2026-08-19).
bool clears = (voters > 0) && (vote != 0.0) && (mag >= m_threshold_open);
string dir = (voters <= 0 && neutrals <= 0) ? "--"
: (clears ? (vote > 0.0 ? "BUY" : "SELL") : "NEUTRAL");
//--- Consolas so the columns line up as the numbers change width - a readout that jitters is one
//--- you have to re-read every time instead of glancing at.
string verdict = prospective
? "-> training, not tradable yet"
: (fires ? "-> TRADE" : "-> no trade");
//--- "2 vote/2 flat" rather than a bare count: which members are Neutral is half of what the
//--- label is watched for during training.
string who = (neutrals >= 0)
? StringFormat("%d vote/%d flat", voters, neutrals)
: StringFormat("%d voter(s)", voters);
string txt = StringFormat("VOTE %s %+5.1f%% peak %5.1f%% need %.0f%% %s %s",
dir, vote, peak, m_threshold_open, who, verdict);
string nm = VOTE_HUD_PREFIX;
if(ObjectFind(0, nm) < 0)
{
//--- ObjectFind is affordable HERE, unlike in the arrow paths: this is ONE object refreshed once
//--- per bar, not thousands created in a sweep. The O(n^2) rule that bans the pre-check there is
//--- about per-object cost in a loop, and applying it blindly here would just leak properties.
ObjectCreate(0, nm, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, nm, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_ANCHOR, ANCHOR_RIGHT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, nm, OBJPROP_YDISTANCE, 18);
ObjectSetInteger(0, nm, OBJPROP_FONTSIZE, 9);
ObjectSetString(0, nm, OBJPROP_FONT, "Consolas");
ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, nm, OBJPROP_HIDDEN, true);
}
ObjectSetString(0, nm, OBJPROP_TEXT, txt);
//--- Colour carries the verdict so the line can be read without parsing it: green/red only when
//--- the vote would actually place an order, grey otherwise. Prospective reads dimmer than "no
//--- trade" so the two are never confused at a glance.
ObjectSetInteger(0, nm, OBJPROP_COLOR,
fires ? (vote > 0.0 ? clrLime : clrRed)
: (prospective ? clrDimGray : clrSilver));
}
//+------------------------------------------------------------------+
//| Repaint the readout from the CURRENT prospective vote. |
//+------------------------------------------------------------------+
void CExpertSignalCustom::RefreshVoteReadout(void)
{
int total = m_filters.Total();
if(total <= 0)
return; // leaf filter: the readout belongs to the aggregate alone
//--- PER-MEMBER NEURON LINES, rendered BEFORE the live-vote defer below: the defer protects the
//--- aggregate VOTE line (a tradable reading must not be repainted with an untradable one), but
//--- the member lines are not tradable readings in the first place - they are the training
//--- telemetry, and freezing them for a whole bar because a live vote exists would re-create the
//--- exact only-moves-once-per-era staleness they were built to end.
int hudLine = 0;
for(int hi = 0; hi < total; hi++)
{
CExpertSignalCustom *hf = m_filters.At(hi);
if(hf == NULL || (m_ignore & (((long)1) << hi)) != 0)
continue;
string hudTxt = hf.DisplayHudLine();
if(hudTxt == "")
continue; // classic ladders and veto filters draw no neuron line
//--- Colour = the member's own current direction (muted tones - these are opinions, not
//--- orders; the vote line's strict green-only-when-it-would-trade rule stays untouched).
double hv = 0.0, hw = 0.0;
hf.ProspectiveVote(hv, hw); // cached: the throttled forward already ran inside DisplayHudLine
if((m_invert & (((long)1) << hi)) != 0)
hv = -hv;
string nm = VOTE_HUD_PREFIX + StringFormat("_m%02d", hudLine);
if(ObjectFind(0, nm) < 0)
{
ObjectCreate(0, nm, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, nm, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_ANCHOR, ANCHOR_RIGHT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, nm, OBJPROP_YDISTANCE, 34 + 14 * hudLine);
ObjectSetInteger(0, nm, OBJPROP_FONTSIZE, 8);
ObjectSetString(0, nm, OBJPROP_FONT, "Consolas");
ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, nm, OBJPROP_HIDDEN, true);
}
ObjectSetString(0, nm, OBJPROP_TEXT, hudTxt);
ObjectSetInteger(0, nm, OBJPROP_COLOR,
(hv > 0.0) ? clrMediumSeaGreen : (hv < 0.0 ? clrIndianRed : clrSilver));
hudLine++;
}
for(int hd = hudLine; hd < m_hudMemberLines; hd++)
ObjectDelete(0, VOTE_HUD_PREFIX + StringFormat("_m%02d", hd));
m_hudMemberLines = hudLine;
if(m_lastLiveVoters > 0)
return; // a real vote is on display; it owns the label until the next bar
double pNum = 0.0, pDen = 0.0;
int pVoters = 0, pFlats = 0;
for(int i = 0; i < total; i++)
{
long mask = ((long)1) << i;
if((m_ignore & mask) != 0)
continue;
CExpertSignalCustom *f = m_filters.At(i);
if(f == NULL)
continue;
double pv = 0.0, pw = 0.0;
if(!f.ProspectiveVote(pv, pw) || pw <= 0.0)
continue;
//--- CONSENSUS: the weight lands in the denominator for every model WITH a decision - a
//--- Neutral dilutes the mean exactly as it does in the live vote and the overlay, so the
//--- label, the arrows and the trade all move together.
pDen += pw;
if(pv == 0.0)
{
pFlats++; // has a decision, and it is Neutral: dilutes the mean, shows in the count
continue;
}
pVoters++;
pNum += ((m_invert & mask) != 0) ? -pv : pv;
}
if(pVoters + pFlats <= 0)
return; // nothing to say yet; leave whatever the label holds
UpdateVoteReadout((pDen > 0.0) ? (pNum / pDen) : 0.0, pVoters, pFlats, true);
}
//+------------------------------------------------------------------+
//| ARM the historical rebuild of the filtered view. |
//| |
//| Called at init and again whenever an era ends, because an era end |
//| is exactly when the answer changes: the nets' weights moved, and |
//| RankTiersFromOos() has just re-derived every tier's vote weight |
//| from that era's holdout. A reconstruction built from the previous |
//| era's weights is a picture of a model that no longer exists. |
//+------------------------------------------------------------------+
void CExpertSignalCustom::StartFilteredOverlay(void)
{
if(DrawUnfilteredSignals)
return; // raw view: the per-model layer owns the chart, nothing to reconstruct
int barsAvail = Bars(m_symbol.Name(), m_period);
if(barsAvail <= 300)
return;
//--- Same bound the "Show signals" rescan uses, for the same reason: full history is not free and
//--- the terminal's own "Max bars in chart" makes anything past it undrawable anyway.
int span = MathMin(SIGNAL_RESCAN_LOOKBACK_BARS, barsAvail);
//--- BOTH BOUNDS ARE SERIES INDICES - 0 is the newest bar and the index counts BACKWARDS in
//--- time.
m_overlayIndex = MathMin(span, barsAvail - 150);
//--- Stop at 2, not 0: bar 0 is still forming and bar 1 is the decision bar the FORWARD path
//--- owns. The handover-time check inside the sweep covers this too, belt and braces.
m_overlayStopIndex = 2;
if(m_overlayIndex < m_overlayStopIndex)
return; // not enough history past the warm-up tail to reconstruct anything
//--- Latch the handover point ONCE. On later rebuilds the cutoff must stay where the EA actually
//--- took over, not creep forward to "now" and start overwriting real decisions with guesses.
if(m_overlayLiveCutoff == 0)
m_overlayLiveCutoff = iTime(m_symbol.Name(), m_period, 0);
m_overlaySweptBars = 0;
m_overlayVotedBars = 0;
m_overlayDrawn = 0;
m_overlayVotedBuy = 0;
m_overlayVotedSell = 0;
m_overlayNmsLastBuyIdx = -1;
m_overlayNmsLastSellIdx = -1;
m_overlayNmsKeptIdx = -1;
m_overlayNmsKeptBuy = false;
m_overlayNmsKeptNet = 0.0;
m_overlayBestNet = 0.0;
//--- The readout's peak resets HERE, at the same regime boundary that resets the census: tier
//--- weights have just been re-derived, and a peak attained under the previous weights is not
//--- comparable to anything the new weights can produce.
m_votePeak = 0.0;
m_overlayPending = true;
}
//+------------------------------------------------------------------+
//| RECONSTRUCT what the filtered view would have shown, one chunk |
//| per call. Returns true while there is more to do. |
//+------------------------------------------------------------------+
bool CExpertSignalCustom::AdvanceFilteredOverlay(const int barBudget)
{
if(!m_overlayPending)
return false;
if(DrawUnfilteredSignals) // switched to the raw view mid-sweep
{
m_overlayPending = false;
return false;
}
int total = m_filters.Total();
int processed = 0;
while(m_overlayIndex >= m_overlayStopIndex && processed < barBudget)
{
//--- STOP CHECK PER BAR, not per slice. The slice bound alone is not a stop check: it bounds
//--- throughput, not latency.
if(IsStopped())
{
m_overlayPending = false;
return false;
}
int idx = m_overlayIndex--;
processed++;
datetime bt = iTime(m_symbol.Name(), m_period, idx);
//--- At or past the handover: the forward path owns these bars. Leave whatever it decided.
if(bt <= 0 || (m_overlayLiveCutoff > 0 && bt >= m_overlayLiveCutoff))
continue;
double num = 0.0, den = 0.0;
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)
continue;
double contribution = 0.0;
bool hasData = false;
if(filter.IsAIFilter())
{
//--- ERA-END SNAPSHOT, not the live cache, and the difference was a chart that
//--- flickered between populated and blank.
hasData = filter.SnapshotVoteAt(idx, contribution);
}
else
if(filter.GetPatternCount() <= 0)
continue; // veto filter (news/session/risk guard) - see below: no vote, no replay
else
{
//--- Replay, with the live journaling state saved across it - see SaveVoteState().
string pl, ps; double nv; int lw, sw, fd;
filter.SaveVoteState(pl, ps, nv, lw, sw, fd);
filter.EvalShift(idx);
filter.Direction();
filter.EvalShift(0);
double signedWeight = (double)(filter.LastLongWeight() - filter.LastShortWeight());
filter.RestoreVoteState(pl, ps, nv, lw, sw, fd);
contribution = filter.ModuleWeight() * signedWeight;
hasData = true; // a ladder always answers; "no match" is an abstention
}
if(!hasData)
continue; // no snapshot entry: this member says nothing about this bar
if((m_invert & mask) != 0)
contribution = -contribution;
num += contribution;
den += filter.ModuleWeight(); // consensus: capable weight, abstainers dilute
}
double net = (den > 0.0) ? (num / den) : 0.0;
//--- Census for the completion line below - see it for why a blank chart has to be able to
//--- say WHY it is blank. The buy/sell split exists because "the vote leans one way" must be
//--- checkable from the log, not inferred from squinting at arrow colours.
if(den > 0.0 && net != 0.0)
{
m_overlayVotedBars++;
if(net > 0.0) m_overlayVotedBuy++;
if(net < 0.0) m_overlayVotedSell++;
if(MathAbs(net) > m_overlayBestNet)
m_overlayBestNet = MathAbs(net);
}
m_overlaySweptBars++;
//--- NO DATA IS NOT A VERDICT. A bar where no member had a snapshot entry (den == 0) says
//--- nothing about the vote there - deleting its arrow on that basis is how the draw/wipe
//--- cycle above erased whole sweeps.
if(den <= 0.0)
continue;
//--- The direction policy (LONG_ONLY/SHORT_ONLY, or the Intelligent drift verdict) gates the
//--- reconstruction exactly as it gates CheckOpenLong/Short live: a blocked side falls into
//--- the else branch below - a real verdict that deletes any standing arrow - because that
//--- trade would not have happened.
if(MathAbs(net) >= m_threshold_open && WarriorDirectionAllows(net > 0.0))
{
bool isBuy = (net > 0.0);
//--- DECLUSTER, same three rules as the per-member arrows (PruneDirectionalClusters) and
//--- for the same reason: consecutive same-direction bars are ONE setup, and a carpet of
//--- arrows on every bar of a trend (observed 2026-08-19, "arrows on every bars") reads as
//--- noise, not signal.
int lastSame = isBuy ? m_overlayNmsLastBuyIdx : m_overlayNmsLastSellIdx;
bool sameRun = (lastSame >= 0 && (lastSame - idx) <= OVERLAY_NMS_WINDOW);
if(isBuy) m_overlayNmsLastBuyIdx = idx; else m_overlayNmsLastSellIdx = idx;
if(sameRun)
{
WarriorDeleteSignalMark(SIG_VOTE_PREFIX + TimeToString(bt));
continue;
}
if(m_overlayNmsKeptIdx >= 0 && (m_overlayNmsKeptIdx - idx) <= OVERLAY_NMS_WINDOW
&& m_overlayNmsKeptBuy != isBuy)
{
if(MathAbs(net) <= m_overlayNmsKeptNet)
{
WarriorDeleteSignalMark(SIG_VOTE_PREFIX + TimeToString(bt));
continue; // weaker side of a flicker at one turn zone
}
//--- this bar is stronger: the earlier opposite arrow is the flicker - take it down
datetime kt = iTime(m_symbol.Name(), m_period, m_overlayNmsKeptIdx);
if(kt > 0)
WarriorDeleteSignalMark(SIG_VOTE_PREFIX + TimeToString(kt));
}
m_overlayNmsKeptIdx = idx;
m_overlayNmsKeptBuy = isBuy;
m_overlayNmsKeptNet = MathAbs(net);
//--- Trigger price, same convention as the live mark above.
double price = iClose(m_symbol.Name(), m_period, idx);
//--- Marked as a reconstruction IN THE TOOLTIP, not just in a comment. Someone reading two
//--- arrows either side of the handover has to be able to tell which one is a record and
//--- which is a replay, and the chart is the only place they will look.
m_overlayDrawn++;
WarriorPlotSignalLevel(SIG_VOTE_PREFIX + TimeToString(bt), bt, (ENUM_TIMEFRAMES)m_period, price,
isBuy, true,
StringFormat("would trade %s @ %s | confidence %.1f%% >= %.1f%% |"
" reconstructed (vote only - order validation not replayed)",
(isBuy ? "BUY" : "SELL"),
DoubleToString(price, m_symbol.Digits()),
MathAbs(net), m_threshold_open));
}
else
WarriorDeleteSignalMark(SIG_VOTE_PREFIX + TimeToString(bt));
}
if(m_overlayIndex < m_overlayStopIndex)
{
m_overlayPending = false;
//--- SAY WHY THE CHART LOOKS THE WAY IT DOES. So the sweep reports its own arithmetic: how
//--- many bars it looked at, how many had any voter at all, the strongest vote it saw, and
//--- the bar that vote had to clear.
bool censusDue = VerboseMode ||
m_overlayDrawn != m_overlayLastLogDrawn ||
MathAbs(m_overlayBestNet - m_overlayLastLogBest) >= 2.0 ||
m_overlaySkippedLogs >= 9;
if(!censusDue)
m_overlaySkippedLogs++;
else
{
m_overlaySkippedLogs = 0;
m_overlayLastLogDrawn = m_overlayDrawn;
m_overlayLastLogBest = m_overlayBestNet;
Print(StringFormat("Filtered view: swept %d bar(s), %d had a voter (%d buy / %d sell), drew %d"
" arrow(s). Strongest vote %.1f%% against a %.1f%% threshold.%s",
m_overlaySweptBars, m_overlayVotedBars, m_overlayVotedBuy, m_overlayVotedSell,
m_overlayDrawn, m_overlayBestNet, m_threshold_open,
(m_overlayVotedBars == 0
? " No member has a completed era yet (snapshots fill at each member's first"
" pass-3 completion) and every classic signal is disabled."
: (m_overlayDrawn == 0
? " The models voted but never strongly enough; this is the vote"
" failing the bar, not the drawing failing."
: ""))));
}
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| The bar timestamp a buffered signal carries, as a datetime. |
//+------------------------------------------------------------------+
datetime SignalTime(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);
}
//+------------------------------------------------------------------+
//| Order buffered signals oldest-first, ready for the DB write. |
//+------------------------------------------------------------------+
void SortSignalsByTime(SignalInfo &signals[])
{
int n = ArraySize(signals);
if(n < 2)
return;
datetime keys[];
ArrayResize(keys, n);
for(int i = 0; i < n; i++)
keys[i] = SignalTime(signals[i]);
for(int i = 1; i < n; i++)
{
SignalInfo item = signals[i];
datetime key = keys[i];
int j = i - 1;
while(j >= 0 && keys[j] > key)
{
signals[j + 1] = signals[j];
keys[j + 1] = keys[j];
j--;
}
signals[j + 1] = item;
keys[j + 1] = key;
}
}
//+------------------------------------------------------------------+
//| Process the signal and update trades |
//+------------------------------------------------------------------+
void CExpertSignalCustom::ProcessBufferedSignals()
{
// Sort the signals array by datetime before processing
SortSignalsByTime(signalBuffer);
if(!dbm.OpenDatabase())
{
Print("Failed to open database.");
return;
}
if(!dbm.BeginTransaction())
{
Print(__FUNCTION__ + ": Failed to begin database transaction, " + IntegerToString(ArraySize(signalBuffer)) + " buffered signal(s) left pending for retry next cycle.");
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(__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();
return;
}
ArrayResize(signalBuffer, 0);
PrintVerbose("Signal buffer cleared after processing.");
// 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.
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
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, double netVote)
{
string Columns[] = {"year", "month", "day", "dayOfWeek", "hour", "minutes", "pattern", "direction", "entryPrice", "exitPrice", "result", "netVote"};
string valArr[] = {IntegerToString(year), IntegerToString(month), IntegerToString(day), IntegerToString(DOW), IntegerToString(hour), IntegerToString(minutes), pattern, direction, DoubleToString(entryPrice, Digits()), DoubleToString(exitPrice, Digits()), result, DoubleToString(netVote, 2)};
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);
int total = m_filters.Total();
double sumModuleWeight = 0.0;
int weightedFilterCount = 0;
//--- Rows at or after 'now' can only exist in a resumed/mixed database and must not leak into
//--- weights mid-backtest; the bound is applied inside SQLite (see FetchWinLossCounts). It replaces
//--- the tester-only array trim the old full-table fetch did here, and is harmless live: a row's
//--- open time is never in the future.
MqlDateTime brokerNow;
TimeCurrent(brokerNow); // broker clock, matching the row stamps since dbVersion 4.0
long nowKey = SignalTimeKey(brokerNow.year, brokerNow.mon, brokerNow.day, brokerNow.hour, brokerNow.min);
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;
//--- POOL PASS. Counting is a pair of SQL aggregates per table (no rows materialize), so the
//--- extra pass costs the same order as the scoring pass below.
int poolWins = 0, poolTotal = 0;
for(int j = 0; j < patternCount; j++)
{
string pPattern = PatternName(j);
int pw = 0, pl = 0;
if(dbm.FetchWinLossCounts(PatternTableName(filterID, pPattern, "Buy"), nowKey, pw, pl))
{
poolWins += pw;
poolTotal += pw + pl;
}
pw = 0;
pl = 0;
if(dbm.FetchWinLossCounts(PatternTableName(filterID, pPattern, "Sell"), nowKey, pw, pl))
{
poolWins += pw;
poolTotal += pw + pl;
}
}
double poolPct = (poolTotal > 0) ? (100.0 * poolWins / poolTotal) : -1.0;
//--- One MIN_TRADES_FOR_WIN_RATE-worth of pseudo-trades: a tier measured at exactly the
//--- minimum ends up half pool / half its own evidence, and the pull halves again with every
//--- doubling of its sample.
int poolWeight = (poolTotal > 0) ? MIN_TRADES_FOR_WIN_RATE : 0;
for(int j = 0; j < patternCount; j++)
{
// Aggregate outcome counts, computed inside SQLite - no rows materialize into MQL arrays,
// so this cycle's cost is flat in table size (the same fix as ProcessSignal's lookups).
string pattern = PatternName(j);
string tableNameBuy = PatternTableName(filterID, pattern, "Buy");
string tableNameSell = PatternTableName(filterID, pattern, "Sell");
int winsBuy = 0, lossesBuy = 0, winsSell = 0, lossesSell = 0;
if(!dbm.FetchWinLossCounts(tableNameBuy, nowKey, winsBuy, lossesBuy))
{
Print(__FUNCTION__ + " Failed to count outcomes in " + tableNameBuy);
continue;
}
if(!dbm.FetchWinLossCounts(tableNameSell, nowKey, winsSell, lossesSell))
{
Print(__FUNCTION__ + " Failed to count outcomes in " + tableNameSell);
continue;
}
int winRateBuy = WinRateFromCounts(winsBuy, lossesBuy, poolPct, poolWeight);
int winRateSell = WinRateFromCounts(winsSell, lossesSell, poolPct, poolWeight);
// 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;
//--- ...but not over a self-ranking filter.
if(moduleWeight > 0 && moduleWeight <= 1 && !filter.SelfRanked())
{
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);
}
//+------------------------------------------------------------------+
//| Win rate from SQL-side outcome counts (see FetchWinLossCounts) |
//+------------------------------------------------------------------+
int CExpertSignalCustom::WinRateFromCounts(const int wins, const int losses, const double priorPct,
const int priorWeight)
{
int totalTrades = wins + losses;
if(totalTrades < MIN_TRADES_FOR_WIN_RATE)
return NO_DATA_WIN_RATE;
//--- Shrunk toward the pool this ladder belongs to - the caller supplies it, and a caller with
//--- no pool passes priorWeight 0 for the raw ratio.
return NormalizeWinRate(ShrunkRatePct((double)wins, (double)totalTrades, priorPct, (double)priorWeight));
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
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;
//--- NO GetFilterID() == "NULL" TEST HERE any more. CSignalNewsFilter, CSignalSessionFilter
//--- and CSignalRiskGuard never set an id, so all three were silently skipped here and in
//--- OnChartEventHandler below.
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;
//--- no id test - see OnTickHandler above.
filter.OnChartEventHandler(id, lparam, dparam, sparam);
}
}
//+------------------------------------------------------------------+
//| Hands a panel command to this signal and every filter under it, |
//| and reports how many acted on it. |
//+------------------------------------------------------------------+
int CExpertSignalCustom::DispatchSignalCommand(const ENUM_SIGNAL_COMMAND cmd)
{
int acted = OnSignalCommand(cmd) ? 1 : 0;
int total = m_filters.Total();
for(int i = 0; i < total; i++)
{
CExpertSignalCustom *filter = m_filters.At(i);
if(filter == NULL)
continue;
acted += filter.DispatchSignalCommand(cmd);
}
return acted;
}
//+------------------------------------------------------------------+
//| How many signals in this subtree carry a given trait. |
//+------------------------------------------------------------------+
int CExpertSignalCustom::CountSignalTrait(const ENUM_SIGNAL_TRAIT trait)
{
int n = HasSignalTrait(trait) ? 1 : 0;
int total = m_filters.Total();
for(int i = 0; i < total; i++)
{
CExpertSignalCustom *filter = m_filters.At(i);
if(filter == NULL)
continue;
n += filter.CountSignalTrait(trait);
}
return n;
}
//+------------------------------------------------------------------+