//+------------------------------------------------------------------+ //| Warrior_EA | //| AnimateDread | //| | //+------------------------------------------------------------------+ #include #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" //--- The meta-labeling VETO, as its own abstraction rather than a virtual every signal carries. #include "Trading\MetaGate.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); } //+------------------------------------------------------------------+ //| THE VOTE NORMALIZATION RULE, and the only copy of it. | //| | //| Live and replay had one each, and they drifted - the replay used | //| ModuleWeight() where live used VoteCapableWeight(), so a META | //| head (a gate, structurally unable to agree) sat in the replay's | //| divisor shrinking every reconstructed vote. Both now Add() here. | //| | //| The rule, in one place: | //| - CAPABLE weight always enters the divisor, contribution or not. | //| An abstainer LOOKED and said nothing, and diluting the | //| consensus is exactly what it should do. | //| - a member that could not look at all (no era-end snapshot, not | //| yet trained, a gate) contributes NO capable weight and so is | //| not in the divisor - the caller simply never Add()s it. | //| - only a non-zero contribution counts as a VOTER, which is what | //| the readout's "N voter(s)" means. | //| | //| WHY NOT CExpertSignal::Direction()'s divisor. Not because the | //| stdlib ignores m_weight - it does not. Each signal weights its | //| OWN conditions (m_weight*(Long-Short)) and every child applies | //| its own in turn, so the weight is respected end to end. The ONE | //| divergence is the normalizer: stdlib divides by the COUNT of | //| participating filters, we divide by the summed CAPABLE weight. | //| | //| Both scale identically with agreement, so stdlib's is a perfectly | //| valid RELATIVE consensus measure. What it is not is an ABSOLUTE | //| one: its output scale is the mean module weight, and we re-derive | //| that from held-out win rates every era. Measured on USDJPY across | //| five eras, the mean module weight ran 0.162 -> 0.285 - a 76% | //| swing - so under /count every vote would have risen 76% with no | //| change in agreement or accuracy, and a fixed threshold would mean | //| something different each era. Dividing by capable weight cancels | //| that factor, which is what makes the result a WIN RATE the | //| threshold, the deploy gate and break-even can all be compared to. | //| | //| The stdlib arithmetic would be exactly right with m_weight left | //| at its 1.0 default and the win rate carried by the PATTERN weight | //| instead - see project_direction_is_a_transaction. That is a live | //| semantic change, not a refactor, so it is not done here. | //+------------------------------------------------------------------+ struct SVoteAccumulator { double num; // signed sum of contributions double capable; // divisor: total capable weight, abstainers included int voters; // members that actually took a side SVoteAccumulator(void) { Reset(); } void Reset(void) { num = 0.0; capable = 0.0; voters = 0; } void Add(const double contribution, const double capableWeight) { capable += capableWeight; if(contribution == 0.0) return; // abstention: dilutes the consensus, is not a voter num += contribution; voters++; } double Net(void) const { return (capable > 0.0) ? (num / capable) : 0.0; } }; //--- 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; //--- NON-VOTING CHILDREN, owned exactly as m_filters owns its own (CArrayObj frees on //--- destruct). Kept out of the voting list so no vote consumer needs a special case - every //--- one of those was a bug waiting, and the replay's divisor was one. CArrayObj m_gates; //--- THE META VETO THIS TREE ANSWERS TO (S3). Held by the ROOT signal - the one CExpert calls //--- CheckOpenLong/Short on - and borrowed by its children through MetaGate() below. This was a //--- file-scope global; a signal tree owning its own gate is not only tidier, it retires the //--- stale-pointer hazard the global needed hand-clearing for (the root is new'd fresh at every //--- re-init, so nothing can survive one). CMetaGate *m_metaGate; //--- Who adopted this signal, NULL for a root. AddFilter() sets it. A raw back-pointer is safe //--- here for the same reason the gate adapter's owner pointer is: m_filters/m_gates free their //--- children, so a parent always outlives them. CExpertSignalCustom *m_parentSignal; //--- 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); //--- DOES THIS SIGNAL CAST VOTES? A gate (the meta head) answers no: its Long/ShortCondition //--- are structurally 0 and it exists to veto entries, not to have an opinion on direction. //--- AddFilter() routes on this, so "only true signalers are filters" is enforced in one place //--- rather than re-checked by every consumer of the list. virtual bool IsVotingSignal(void) const { return true; } //--- ADOPTION. AddFilter() sets this on every child it takes, voter or gate, so a child can //--- reach the tree it belongs to. That is how an AI filter finds the root's meta gate at era- //--- verdict time: it sits deep in the training code and has no other route up. void SetParentSignal(CExpertSignalCustom *parent) { m_parentSignal = parent; } //--- The meta veto, installed on the root once the meta head exists (see Warrior_EA's OnInit). void SetMetaGate(CMetaGate *gate) { m_metaGate = gate; } CMetaGate *OwnMetaGate(void) const { return m_metaGate; } //--- THE GATE THIS SIGNAL ANSWERS TO: its own if it is a root, its parent's if it was adopted. //--- One level, because AddFilterToSignal is only ever called on the root; a deeper nesting //--- would answer NULL, which on this path means fail-open - the doctrine everywhere else too. CMetaGate *MetaGate(void) const { if(CheckPointer(m_metaGate) != POINTER_INVALID) return m_metaGate; return (CheckPointer(m_parentSignal) != POINTER_INVALID) ? m_parentSignal.OwnMetaGate() : NULL; } virtual bool AddFilter(CExpertSignal *filter); //--- THE ADMINISTRATIVE TREE: every child this node owns, voter or not. m_filters is the VOTING //--- list; indicators, ticks, panel commands and trait counts must reach the whole tree, and a //--- non-voter that is missing from them silently stops training or stops answering the panel. int ChildSignalCount(void) const { return m_filters.Total() + m_gates.Total(); } CExpertSignalCustom *ChildSignalAt(const int i) { int nf = m_filters.Total(); return (CExpertSignalCustom *)((i < nf) ? m_filters.At(i) : m_gates.At(i - nf)); } 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); //--- 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. //--- The replay half of Direction(), with live's divisor rule. See the definition for why the //--- two cannot simply be one call today. double HistoricalNetVote(const int idx, double &capableOut); 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; }; //--- Direction()'s two side effects, named so the transaction it performs is visible at the //--- call site instead of buried in the loop that also does the arithmetic. void JournalFilterPatterns(CExpertSignalCustom *filter, const MqlDateTime &brokerTime); void DrawFilterRawView(CExpertSignalCustom *filter); 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_metaGate(NULL), m_parentSignal(NULL), 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; //--- WHOLE TREE: a gate is still a signal and needs its indicators and series. It is missing from //--- CExpertSignal::InitIndicators below (that walks m_filters), which is why it is done here. int total = ChildSignalCount(); //--- gather information about using of timeseries for(int i = 0; i < total; i++) { filter = ChildSignalAt(i); if(filter == NULL) continue; 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 = ChildSignalAt(i); if(filter == NULL) continue; 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; //--- ONLY TRUE SIGNALERS GO IN THE VOTING LIST. A non-voter still needs everything else a //--- signal needs - indicators, ticks, panel commands, traits - so it is adopted into m_gates //--- and the administrative walks pick it up from there. Routing here means the EA's init code //--- stays one uniform AddFilterToSignal() call per signal. CExpertSignalCustom *asCustom = dynamic_cast(filter); //--- Adoption is adoption: a child knows the tree it belongs to whichever list it lands in. //--- That back-pointer is how an AI filter reaches the root's meta gate at era-verdict time - //--- it is deep in the training code, has no other route up, and a file-scope global used to be //--- that route. if(asCustom != NULL) asCustom.SetParentSignal(GetPointer(this)); if(asCustom != NULL && !asCustom.IsVotingSignal()) { if(!m_gates.Add(filter)) return false; filter.EveryTick(m_every_tick); filter.Magic(m_magic); return true; } if(!m_filters.Add(filter)) return false; filter.EveryTick(m_every_tick); filter.Magic(m_magic); CExpertSignalCustom *customFilter = dynamic_cast(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). CMetaGate *metaGate = MetaGate(); if(CheckPointer(metaGate) != POINTER_INVALID) { double mgP = -1.0, mgBe = -1.0; if(CMetaGate::Blocks(metaGate.Evaluate(isLong, m_direction, mgP, mgBe, 1))) { 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; } //+------------------------------------------------------------------+ //| SIDE EFFECT 1 of Direction(): journal this filter's matched | //| patterns to the signal DB. | //| | //| Per side, 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 row on weight | //| would freeze a 0%-win-rate pattern out of the very table that | //| could ever raise it back. The net vote is stored as data | //| (netVote column), not used as a drop filter. | //+------------------------------------------------------------------+ void CExpertSignalCustom::JournalFilterPatterns(CExpertSignalCustom *filter, const MqlDateTime &brokerTime) { if(filter == NULL || !m_useDatabase) return; string filterID = filter.GetFilterID(); if(filterID == "NULL") return; double filterNetVote = filter.LastNetVote(); string patternLong = filter.GetActivePatternLong(); string patternShort = filter.GetActivePatternShort(); if(patternLong != "NULL") BufferNewTickSignal(filterID, patternLong, "Buy", brokerTime, m_symbol.Ask(), filterNetVote); if(patternShort != "NULL") BufferNewTickSignal(filterID, patternShort, "Sell", brokerTime, m_symbol.Bid(), filterNetVote); } //+------------------------------------------------------------------+ //| SIDE EFFECT 2 of Direction(): the RAW per-model view. | //| | //| Classic filters only - an AI member's raw arrows come from its | //| own cache, not from a live tick. Erasing when neither ladder | //| matched is deliberate: a stale arrow on a bar that no longer | //| matches is a lie about what the model sees now. | //+------------------------------------------------------------------+ void CExpertSignalCustom::DrawFilterRawView(CExpertSignalCustom *filter) { if(filter == NULL || !DrawUnfilteredSignals || filter.IsAIFilter()) return; 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); } //+------------------------------------------------------------------+ //| 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); //--- Seeded with this signal's OWN vote, on the same condition it always was: a filter with //--- children of its own is a member of its own consensus. Same accumulator the replay uses - //--- see SVoteAccumulator for why there is exactly one of these. SVoteAccumulator vote; vote.Add(result, (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; } JournalFilterPatterns(filter, brokerTime); double direction = filter.Direction(); //--- AFTER the Direction() call, not beside the journaling: the raw view draws what this //--- evaluation just matched, and before the call that is still last bar's match. DrawFilterRawView(filter); 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; 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. long mask = ((long)1) << i; double signedDir = ((m_invert & mask) != 0) ? -direction : direction; vote.Add(signedDir, filter.VoteCapableWeight()); } } //--- An aborted tick published nothing, so it has no voters and no net - see the rollback above. int number = aborted ? 0 : vote.voters; if(!aborted) result = vote.Net(); //--- Normalized by SVoteAccumulator above: the divisor is the CAPABLE weight, so the result reads //--- as "win-rate estimate x fraction of the ensemble's trust that agrees, net". //--- 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; } //+------------------------------------------------------------------+ //| THE VOTE ON A HISTORICAL BAR - one aggregation rule, one divisor. | //| | //| This is the replay half of Direction(). It is separate for ONE | //| reason: Direction() is not a query, it is a transaction. It | //| journals DB rows, draws raw arrows, folds the result into an | //| intra-second averaging window, consumes one-shot per-filter vote | //| state and refreshes the live readout. Every one of those is | //| wrong on a bar from three weeks ago, which is why the classic | //| replay below has to bracket its Direction() call in a six-field | //| save/restore - that bracket IS the evidence, and it goes away | //| when Direction() is split into a pure vote plus its side effects. | //| | //| What must NOT differ between here and live is the arithmetic, and | //| it did: this loop used ModuleWeight() as the divisor while live | //| uses VoteCapableWeight(). A meta head (structurally incapable of | //| voting) and an untrained member both return 0 from the latter and | //| their full weight from the former, so every reconstructed vote | //| was shrunk by members that could never agree. The comment on the | //| old line even said "capable weight" while the code said | //| ModuleWeight - read the code for the value, the comment for the | //| why, and when they disagree the code is what shipped. | //| | //| capableOut returns the divisor so the caller can tell "nobody had | //| anything to say about this bar" (0) from "the vote was neutral". | //+------------------------------------------------------------------+ double CExpertSignalCustom::HistoricalNetVote(const int idx, double &capableOut) { SVoteAccumulator vote; int total = m_filters.Total(); for(int i = 0; i < total; i++) { long mask = ((long)1) << i; if((m_ignore & mask) != 0) continue; CExpertSignalCustom *filter = m_filters.At(i); if(filter == NULL) 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): 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 } //--- NO SNAPSHOT IS NOT AN ABSTENTION. An abstainer looked at the bar and said nothing, and //--- must dilute the consensus; a member with no snapshot has not looked, and must not be in //--- the divisor at all - see g_warriorOverlayReadyMask. if(!hasData) continue; if((m_invert & mask) != 0) contribution = -contribution; vote.Add(contribution, filter.VoteCapableWeight()); } capableOut = vote.capable; return vote.Net(); } //+------------------------------------------------------------------+ //| 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 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 den = 0.0; double net = HistoricalNetVote(idx, den); //--- 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) { //--- WHOLE TREE, not just the voters: OnTickHandler is what drives each AI signal's training, //--- and a gate left out of it silently stops learning. int total = ChildSignalCount(); for(int i = 0; i < total; i++) { CExpertSignalCustom *filter = ChildSignalAt(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 = ChildSignalCount(); // whole tree - a gate has panel controls like any signal for(int i = 0; i < total; i++) { CExpertSignalCustom *filter = ChildSignalAt(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 = ChildSignalCount(); // whole tree - a panel command must reach a gate too for(int i = 0; i < total; i++) { CExpertSignalCustom *filter = ChildSignalAt(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 = ChildSignalCount(); // the panel acts on the whole tree, so it must count it for(int i = 0; i < total; i++) { CExpertSignalCustom *filter = ChildSignalAt(i); if(filter == NULL) continue; n += filter.CountSignalTrait(trait); } return n; } //+------------------------------------------------------------------+