2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//+------------------------------------------------------------------+
# include <Expert\ExpertSignal.mqh>
# include "..\System\NewBar.mqh"
# include "..\Structures\tradeRecordStructure.mqh"
# include "..\Structures\signalInfoStructure.mqh"
# include "..\Variables\ConfidenceBridge.mqh"
# include "..\System\TradeChecks.mqh"
//--- Enumerations
# include "..\Enumerations\GlobalEnums.mqh"
//
2026-08-12 15:20:33 -04:00
# 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
2026-08-09 14:51:59 -04:00
# 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.
//--- WAS 2.0, LOWERED TO 0.5 on 2026-07-31 when the stop moved off the swing anchor. At 2.0 it existed
//--- because a swing-anchored stop could land arbitrarily close to the entry (a shallow pullback puts
//--- the swing right at the fill), so the distance needed a floor unrelated to the chosen multiple.
//--- An entry-anchored stop is exactly SL_Mode*ATR by construction and cannot collapse, so keeping the
//--- floor at 2.0 would have quietly overridden SL_ATR_x1 to 2*ATR - making that input a lie AND
//--- forcing TP >= 4*ATR just to clear what was then a 1:2 minimum-reward:risk rejection. That is the
//--- same interaction that once rejected 100% of setups on every symbol (see TP_INTELLIGENT_BASE_RR).
//--- The rejection filter itself was removed on 2026-08-09; this floor still matters, because it is
//--- what stops SL_Mode from being silently overridden.
# 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). Kept as a local macro
//--- rather than referencing the enum name so this header stays independent of InputEnums.mqh's include
//--- order, exactly like m_confidence_source being an int (see Variables\ConfidenceBridge.mqh).
# define SL_INTELLIGENT_MODE ( -1 )
# define TP_INTELLIGENT_MODE ( -1 )
//--- 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. ENTRY_PREV_SWING is
//--- unaffected and still uses the swing prices; that is why they are still computed here.
//--- Intelligent (AI-confidence) SL/TP shaping, driven by EffectiveConfidence() (a 0..1 magnitude, see
//--- CExpertSignalAIBase::AIConfidence/DBConfidence per Confidence_Source):
//--- - SL starts SL_INTELLIGENT_BASE_MULT beyond the swing and TIGHTENS by up to AI_SL_TIGHTEN_FACTOR
//--- (30%) as confidence -> 1: a high-conviction setup gets a tighter stop, a marginal one keeps the
//--- full ATR cushion. Still floored at MIN_SL_ATR_MULTIPLIER above.
//--- - TP is a multiple of THIS TRADE'S OWN RISK (the final entry-to-stop distance), not of ATR: it
//--- starts at TP_INTELLIGENT_BASE_RR and WIDENS by up to AI_TP_WIDEN_FACTOR (+100%, i.e. 2x) as
//--- confidence -> 1, so RR runs 2.5 (zero confidence) to 5.0 (full conviction).
//--- WHY risk-relative and not ATR-relative: SL is swing-anchored PLUS padding, so its distance
//--- grows with the swing gap, while an ATR-from-entry TP does not. Those two were decoupled when
//--- TP moved off the opposite-swing anchor (commit 0f09588), and nothing re-checked the result
//--- against the then-active minimum reward:risk: with confidence pinned at 0 (AI disabled - the shipped
//--- default) the old TP_INTELLIGENT_BASE_MULT of 3.0 produced reward = 3*ATR against a risk that
//--- MIN_SL_ATR_MULTIPLIER alone floors at 2*ATR, so `reward < 2.0*risk` was ALWAYS true and
//--- OpenParams() rejected 100% of setups on every symbol and timeframe - the EA could not place a
//--- single trade. Deriving TP from the realised risk restores the coupling the swing-anchored TP
//--- used to provide. The 1:2 rejection filter that made this coupling load-bearing is gone as of
//--- 2026-08-09, but the coupling is kept: a TP derived from the trade's own risk is the correct
//--- shape regardless of whether anything downstream is checking the ratio.
# 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. ENTRY_INTELLIGENT_BASE_MULT is the DEEPEST limit
//--- pullback (in ATRs, at zero confidence); it shrinks linearly to 0 (market fill) as confidence -> 1.
# define ENTRY_INTELLIGENT_MODE ( -100 )
# define ENTRY_PREV_SWING_MODE ( -101 )
# define ENTRY_INTELLIGENT_BASE_MULT 2.0
//
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 ) ;
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
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 ) ;
2026-08-09 14:51:59 -04:00
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 ) ;
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
void BufferNewTickSignal ( string filterID , string pattern , string bias , const MqlDateTime & gmtTime , double entryPrice , double netVote ) ;
2026-08-09 14:51:59 -04:00
string PatternTableName ( string filterID , string pattern , string direction ) ;
string PatternName ( int patternIndex ) { return " Pattern_ " + IntegerToString ( patternIndex ) ; }
SignalInfo signalBuffer [ ] ;
protected :
bool m_prohibition_signal ;
bool m_useDatabase ;
CiATR m_ATR ; // ATR indicator
string m_id ;
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//--- 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).
//--- Direction() snapshots the scratch into the per-side slots below around each ladder call, so a
//--- short-side match can no longer overwrite what the long ladder found (and vice versa). The DB
//--- journaling reads ONLY the per-side slots.
2026-08-09 14:51:59 -04:00
string m_active_pattern ;
string m_active_direction ;
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
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"
2026-08-12 12:02:05 -04:00
//--- 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(). NOTE: this is a record of the DECISION LAYER'S state at log time, not an
//--- objective measure - the per-pattern weights inside it are themselves adjusted by
//--- UpdateSignalsWeights(), so its scale drifts as ranking updates land. The objective part of a
//--- row is the pattern/direction/price/result columns; netVote is the decision context they were
//--- logged under.
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
double m_lastNetVote ;
2026-08-12 15:20:33 -04:00
int m_maxTableRows ; // per-table row cap, from the DB_MaxRowsPerTable input
2026-08-09 14:51:59 -04:00
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)
//--- 0..1 min. AI confidence, reversed against the position, required to trigger an early exit. Set
//--- from the SAME Min_Vote_Close input that drives m_threshold_close, just rescaled - see that
//--- input's declaration comment (Variables\Inputs.mqh) for why one number governs both exit routes.
//--- There is deliberately no companion on/off flag: Min_Vote_Close = Disabled resolves to 1.01 here,
//--- which no softmax confidence can reach, so the route switches itself off.
double m_ai_exit_threshold ;
double m_dbConfidence ; // last average normalized DB win-rate across active filters
//--- Direction()'s per-second aggregation state. MUST be per-instance, not function-local statics -
//--- Direction() is inherited as-is (not overridden) by every CExpertSignalCustom subclass that
//--- doesn't provide its own (the root "signal" object AND CExpertSignalAIBase, so PAI/CONV/LSTM),
//--- meaning they'd all share one compiled function body. Function-local statics there would be a
//--- single instance shared across the root signal and every AI filter, each stomping on the
//--- others' in-progress per-second average instead of keeping their own.
//--- The window key is a full GMT timestamp, NOT MqlDateTime.sec. Keying on the 0-59 seconds FIELD
//--- alone made two calls a minute (or an hour, or a day) apart look like the same window: with
//--- Expert_EveryTick=false every call lands on a bar open, where sec is always 0, so the window
//--- never rolled over and every bar's vote accumulated into one ever-growing average that decayed
//--- toward 0 as the run went on. A full timestamp rolls the window over on every new second, which
//--- is what "average the votes cast within one second" was always meant to mean.
datetime m_directionCurrentSecond ;
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
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
// GetActivePatternLong()/Short(), never consumed/reset by
// a read - a pure peek, safe for a parent to poll every tick.
2026-08-09 14:51:59 -04:00
public :
CExpertSignalCustom ( void ) ;
~ CExpertSignalCustom ( void ) ;
virtual bool AddFilter ( CExpertSignal * filter ) ;
virtual bool CheckOpenLong ( double & price , double & sl , double & tp , datetime & expiration ) override ;
virtual bool CheckOpenShort ( double & price , double & sl , double & tp , datetime & expiration ) override ;
virtual bool CheckCloseLong ( double & price ) override ;
virtual bool CheckCloseShort ( double & price ) override ;
bool OpenParams ( bool isLong , double & price , double & sl , double & tp , datetime & expiration ) ; // Added for generalized parameter calculation
virtual bool OpenLongParams ( double & price , double & sl , double & tp , datetime & expiration ) override ;
virtual bool OpenShortParams ( double & price , double & sl , double & tp , datetime & expiration ) override ;
virtual bool ValidationSettings ( void ) override ;
virtual bool InitIndicators ( CIndicators * indicators ) override ;
void Entry_Multiplier ( double entry_multiplier ) { m_entry_multiplier = entry_multiplier ; }
void Periods ( int periods ) { m_periods = periods ; }
void SLMode ( int value ) { m_sl_mode = value ; }
void TPMode ( int value ) { m_tp_mode = value ; }
void ConfidenceSource ( int value ) { m_confidence_source = value ; }
void AIExitThreshold ( double value ) { m_ai_exit_threshold = value ; }
int LastFiredDirection ( void ) { return m_lastFiredDirection ; }
feat(meta): self-contained corpus - the META chart sweeps the real classic ladders over its own history
The user should not need a tester corpus run per symbol. Every pattern
condition in Signals\Signal{MA,RSI,MACD,Ichimoku}.mqh anchors its reads on
`int idx = StartIndex()` with zero hardcoded indices (verified), so a
name-hiding StartIndex override + EvalShift(i) on CExpertSignalCustom makes
the EXACT live ladder code answer "what would you have fired at bar i" -
the silent-divergence trap that justified the DB corpus does not exist on
this path, and neither do the GMT-offset ambiguity, the DB row caps, or
the wipe procedure.
- CExpertSignalCustom: m_evalShift + StartIndex()/EvalShift() +
SweepPrepare(bars) (deep-resizes the shared price series); the four
classic signal classes override SweepPrepare to deep-resize their own
indicator buffers.
- CSignalMETA::BuildCorpusBySweep: per bar x per source filter, run
Direction() shifted, harvest the per-side pattern slots + netVote into
the same corpus arrays the DB loader fills; entry=bar open so
MetaPrepareEra's resolution matches at offset +0 with zero price error.
DB corpus remains the fallback when classic filters are disabled.
- Warrior_EA.mq5: META gets the enabled classic filters as candidate
sources (family ids match the descriptor one-hot).
- UseDatabaseRanking default false -> true (user request): a META chart
journals + ranks out of the box.
Workflow per symbol is now: attach ONE chart with AIType=META (optionally
Meta_ExportDataset=true for the offline pool) - candidates, labels,
training and export all happen in place, ~10 seconds of sweep instead of a
tester run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:19:43 -04:00
//--- HISTORICAL EVALUATION SHIFT (meta-labeling candidate sweep). Every pattern condition in every
//--- signal class anchors its reads on `int idx = StartIndex();` (verified: no hardcoded indices
//--- anywhere in Signals\Signal{MA,RSI,MACD,Ichimoku}.mqh), so overriding StartIndex to return a
//--- historical bar index makes the REAL, live ladder code evaluate "as of that bar" - no
//--- condition mirroring, no divergence trap. Non-zero only inside CSignalMETA's corpus sweep;
//--- 0 = normal live behaviour (base rule: every_tick ? 0 : 1). Name-hiding is sufficient: the
//--- stock StartIndex is non-virtual, but every condition body lives in classes BELOW this one,
//--- so their calls resolve here.
int m_evalShift ;
void EvalShift ( const int shift ) { m_evalShift = shift ; }
int StartIndex ( void ) { return ( m_evalShift > 0 ? m_evalShift : ( 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 ;
}
2026-08-09 14:51:59 -04:00
// 0.0 = no AI confidence available (pure rule-based); overridden in
// CExpertSignalAIBase to return the live signal's confidence in [0,1].
virtual double AIConfidence ( void ) { return 0.0 ; }
// Signed version of AIConfidence: sign gives direction (+ buy, - sell), used for
// AI-driven early exit. 0.0 = no AI filter (base rule-based class never exits early).
virtual double SignedAIConfidence ( void ) { return 0.0 ; }
// Returns this instance's own SignedAIConfidence() when it IS an AI signal, otherwise the live
// value the AI signal publishes each tick (g_LiveAISignedConfidence, see
// CExpertSignalAIBase::ScheduleTrainingIfNeeded). This is what lets the non-AI aggregate/root
// signal - the object CExpert actually calls to size, scale, and manage every trade - see REAL AI
// confidence instead of the constant 0 its own SignedAIConfidence() returns. Without it,
// Intelligent MM, AI SL/TP scaling, and AI-exit were all running with their AI component pinned to 0.
double LiveSignedConfidence ( void ) ;
// Combines AIConfidence()/DBConfidence() per m_confidence_source into a single 0..1
// magnitude, used to scale SL/TP and (Intelligent MM) lot size.
double EffectiveConfidence ( void ) ;
double DBConfidence ( void ) { return m_dbConfidence ; }
virtual void ApplyPatternWeight ( int patternNumber , int weight ) { } ;
void ID ( string id ) { m_id = id ; }
virtual string GetFilterID ( void ) { return m_id ; } ;
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//--- 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 ) ;
double LastNetVote ( void ) { return m_lastNetVote ; }
2026-08-09 14:51:59 -04:00
virtual int GetPatternCount ( void ) { return m_pattern_count ; } ;
virtual double Direction ( void ) override ;
//--- Vote lifecycle hooks, for filters whose LongCondition()/ShortCondition() consume one-shot state
//--- when they fire. No filter does today - the AI signals' alternation gate was the only user and was
//--- removed with the triple-barrier relabel (see CExpertSignalAIBase) - so both hooks are currently
//--- inert. Kept because the rollback contract below is the non-obvious part and is easy to get wrong
//--- if a future one-shot vote is added without it. Direction()
//--- calls BeginVote() on itself before polling its own conditions, and RevokeVote() on any CHILD whose
//--- vote it then throws away. Without this, a vote that Hybrid's quorum suppressed still burned the
//--- child's gate: PAI flipping Buy alone on bar 10 consumed its Buy gate, so when CONV flipped Buy on
//--- bar 12 PAI was already gated to 0 and the count was STILL 1 of the 2 required - in practice all
//--- three models had to flip on the very same bar, and every near-miss cost a model that direction
//--- until the opposite signal arrived. Deliberately NOT revoked on the prohibition path: a vetoed tick
//--- still blocks only OPENING (see CheckOpenPosition), and the vote does reach m_direction where
//--- CheckClosePosition can act on it, so that vote was used, not discarded. Base = no-op.
virtual void BeginVote ( void ) { }
virtual void RevokeVote ( void ) { }
bool UpdateSignalsWeights ( void ) ;
2026-08-12 18:53:04 -04:00
int WinRateFromCounts ( const int wins , const int losses ) ;
2026-08-09 14:51:59 -04:00
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 ; } ;
2026-08-12 15:20:33 -04:00
void MaxTableRows ( int value ) { m_maxTableRows = MathMax ( 1 , value ) ; } ;
2026-08-09 14:51:59 -04:00
//--- 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 " ) ,
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
m_active_pattern_long ( " NULL " ) ,
m_active_pattern_short ( " NULL " ) ,
m_lastNetVote ( 0.0 ) ,
feat(meta): self-contained corpus - the META chart sweeps the real classic ladders over its own history
The user should not need a tester corpus run per symbol. Every pattern
condition in Signals\Signal{MA,RSI,MACD,Ichimoku}.mqh anchors its reads on
`int idx = StartIndex()` with zero hardcoded indices (verified), so a
name-hiding StartIndex override + EvalShift(i) on CExpertSignalCustom makes
the EXACT live ladder code answer "what would you have fired at bar i" -
the silent-divergence trap that justified the DB corpus does not exist on
this path, and neither do the GMT-offset ambiguity, the DB row caps, or
the wipe procedure.
- CExpertSignalCustom: m_evalShift + StartIndex()/EvalShift() +
SweepPrepare(bars) (deep-resizes the shared price series); the four
classic signal classes override SweepPrepare to deep-resize their own
indicator buffers.
- CSignalMETA::BuildCorpusBySweep: per bar x per source filter, run
Direction() shifted, harvest the per-side pattern slots + netVote into
the same corpus arrays the DB loader fills; entry=bar open so
MetaPrepareEra's resolution matches at offset +0 with zero price error.
DB corpus remains the fallback when classic filters are disabled.
- Warrior_EA.mq5: META gets the enabled classic filters as candidate
sources (family ids match the descriptor one-hot).
- UseDatabaseRanking default false -> true (user request): a META chart
journals + ranks out of the box.
Workflow per symbol is now: attach ONE chart with AIType=META (optionally
Meta_ExportDataset=true for the offline pool) - candidates, labels,
training and export all happen in place, ~10 seconds of sweep instead of a
tester run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:19:43 -04:00
m_evalShift ( 0 ) ,
2026-08-12 15:20:33 -04:00
m_maxTableRows ( MAX_TABLE_ROWS ) ,
2026-08-09 14:51:59 -04:00
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 ) ,
//--- seeded unreachable (>1.0), so an instance whose AIExitThreshold() was never set from
//--- Min_Vote_Close cannot early-exit on a stale default rather than on the trader's setting
m_ai_exit_threshold ( 1.01 ) ,
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 ( ) ;
return ( own ! = 0.0 ) ? own : g_LiveAISignedConfidence ;
}
double CExpertSignalCustom : : EffectiveConfidence ( void )
{
g_AISignedConfidence = LiveSignedConfidence ( ) ;
g_DBConfidence = m_dbConfidence ;
return CombinedConfidence ( m_confidence_source ) ;
}
//+------------------------------------------------------------------+
//| Destructor |
//+------------------------------------------------------------------+
CExpertSignalCustom : : ~ CExpertSignalCustom ( void )
{
ArrayFree ( signalBuffer ) ;
}
//+------------------------------------------------------------------+
//| Tester-only trade rejection tracing |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : ShouldTraceTradeRejections ( void ) const
{
return VerboseMode ;
}
void TraceSignalRejection ( const string key , const string message )
{
if ( ! VerboseMode )
return ;
TCLog ( " signal-reject: " + key , message ) ;
}
//+------------------------------------------------------------------+
//| Single source of truth for the per-pattern/direction table name |
//+------------------------------------------------------------------+
string CExpertSignalCustom : : PatternTableName ( string filterID , string pattern , string direction )
{
return filterID + " _ " + pattern + " _ " + direction ;
}
//+------------------------------------------------------------------+
//| Helper function to check value ranges |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : InRange ( double value , double min , double max )
{
return value > = min & & value < = max ;
}
//+------------------------------------------------------------------+
//| Validation settings protected data |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : ValidationSettings ( void )
{
if ( ! CExpertSignal : : ValidationSettings ( ) )
return false ;
// Simplified checks using the InRange helper
if ( ! InRange ( m_periods , 0 , 200 ) )
{
printf ( __FUNCTION__ " : ATR Periods must be 0-200 " ) ;
return false ;
}
if ( ! InRange ( StartIndex ( ) , 0 , 200 ) )
{
printf ( __FUNCTION__ " : ATR shift must be 0-200 " ) ;
return false ;
}
return true ;
}
//+------------------------------------------------------------------+
//| Create indicators |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : InitIndicators ( CIndicators * indicators )
{
//--- check pointer
if ( indicators = = NULL )
return ( false ) ;
//---
CExpertSignal * filter ;
int total = m_filters . Total ( ) ;
//--- gather information about using of timeseries
for ( int i = 0 ; i < total ; i + + )
{
filter = m_filters . At ( i ) ;
m_used_series | = filter . UsedSeries ( ) ;
}
//--- create required timeseries
if ( ! CExpertBase : : InitIndicators ( indicators ) )
return ( false ) ;
//--- initialization of indicators and timeseries in the additional filters
for ( int i = 0 ; i < total ; i + + )
{
filter = m_filters . At ( i ) ;
filter . SetPriceSeries ( m_open , m_high , m_low , m_close ) ;
filter . SetOtherSeries ( m_spread , m_time , m_tick_volume , m_real_volume ) ;
if ( ! filter . InitIndicators ( indicators ) )
return ( false ) ;
}
if ( ! indicators . Add ( GetPointer ( m_ATR ) ) | | ! m_ATR . Create ( m_symbol . Name ( ) , m_period , m_periods ) | | ! CExpertSignal : : InitIndicators ( indicators ) )
{
printf ( __FUNCTION__ " : error initializing indicators " ) ;
return false ;
}
return true ;
}
//+------------------------------------------------------------------+
//| Setting an additional filter |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : AddFilter ( CExpertSignal * filter )
{
if ( filter = = NULL )
return false ;
if ( ! filter .Init ( m_symbol , m_period , m_adjusted_point ) )
return false ;
if ( ! m_filters . Add ( filter ) )
return false ;
filter . EveryTick ( m_every_tick ) ;
filter . Magic ( m_magic ) ;
CExpertSignalCustom * customFilter = dynamic_cast < CExpertSignalCustom * > ( filter ) ;
if ( customFilter ! = NULL )
{
string filterID = customFilter . GetFilterID ( ) ;
if ( filterID ! = " NULL " & & m_useDatabase )
{
int patternCount = customFilter . GetPatternCount ( ) ;
for ( int i = 0 ; i < patternCount ; i + + )
{
string tableNameBuy = PatternTableName ( filterID , PatternName ( i ) , " Buy " ) ;
string tableNameSell = PatternTableName ( filterID , PatternName ( i ) , " Sell " ) ;
dbm . CreateTable ( tableNameBuy , tableschema ) ; // Create table for Buy direction
dbm . CreateTable ( tableNameSell , tableschema ) ; // Create table for Sell direction
}
}
}
return true ;
}
//+------------------------------------------------------------------+
//| Which order type a given entry price will actually produce. |
//| CExpertTrade::Buy()/Sell() route on price vs ask/bid +- the |
//| SYMBOL_TRADE_STOPS_LEVEL: further out than that in the pending |
//| direction becomes a stop/limit order, anything nearer becomes a |
//| market fill. Reproducing that decision here (rather than assuming |
//| "Entry_Multiplier != MARKET means pending") is what lets |
//| OpenParams() validate the SL/TP against the right reference |
//| price - the article measures a market order's stops from the |
//| OPPOSITE side of the spread and a pending order's from its own |
//| activation price, and those are different numbers. |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE CExpertSignalCustom : : ResolveOrderType ( bool isLong , double price )
{
if ( price < = 0.0 )
return ( isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL ) ;
double stops = TCStopsLevel ( m_symbol . Name ( ) ) ;
if ( isLong )
{
double ask = m_symbol . Ask ( ) ;
if ( price > ask + stops )
return ( ORDER_TYPE_BUY_STOP ) ;
if ( price < ask - stops )
return ( ORDER_TYPE_BUY_LIMIT ) ;
return ( ORDER_TYPE_BUY ) ;
}
double bid = m_symbol . Bid ( ) ;
if ( price > bid + stops )
return ( ORDER_TYPE_SELL_LIMIT ) ;
if ( price < bid - stops )
return ( ORDER_TYPE_SELL_STOP ) ;
return ( ORDER_TYPE_SELL ) ;
}
//+------------------------------------------------------------------+
//| 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. The validity
// guards below therefore reject the setup only when it genuinely depends on a swing: previously an
// unsynced or thin history rejected EVERY trade, including configurations whose levels no longer
// reference a swing at all. 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. The resulting price is what
// CExpertTrade::Buy/Sell routes into a market / limit / stop order (it compares price to
// ask/bid +- the broker stop-level itself), so a near-market price simply fills at market.
int entryMode = ( int ) m_entry_multiplier ;
if ( entryMode = = ENTRY_PREV_SWING_MODE )
price = m_symbol . NormalizePrice ( isLong ? lowest_low : highest_high ) ;
else if ( entryMode = = ENTRY_INTELLIGENT_MODE )
{
// 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.
// Anchored to `price`, NOT to base_price: with a pending entry (Entry_Multiplier / ENTRY_*),
// `price` is where the trade will actually fill, and the risk that Money sizes against is
// entry-to-stop. Measuring from the current bid/ask instead would make the realised risk differ
// from the configured multiple by the whole entry offset.
fix: live trades now use the geometry the gate certifies; perf: BN kernels
Three changes, one theme: the trade placed, the trade graded, and the trade
computed are now the same trade.
1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier
pair reached the LABELS only - OpenParams still placed orders at the enum
geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before
3.33*ATR above break-even" about trades the EA never placed. Published via
g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick
contract as the confidence globals, because OpenParams runs on the root signal
which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at
era 0, and the .cfg adoption a deployed model takes. Overrides both legs and
both Intelligent modes - the certificate is exact or it is nothing. TP is
ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot
reshape the certified target.
2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl -
forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a
line-for-line transcription of the host implementation (NormalizeHost /
HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the
exact moment-write ordering. The host copies remain the runtime for the DLL
and pure-MQL5 tiers and the reference the kernels must match.
Because this box has no OpenCL platform, the safety story is layered:
- shim validation: kernels compiled as C and driven against a fp64 host
transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the
frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff
0.132 vs tolerance 1.0
- in-situ self-check: each kernel is compared against its host twin ON FIRST
USE on the real device (SelfCheckBn*), covering what the shim cannot - arg
indices and buffer bindings. Any disagreement resyncs from the good copy,
latches all BN kernels off process-wide, and training continues host-side.
A transcription bug costs a warning and some speed, never a poisoned .nnw.
- sync discipline: BatchOptions is now a CBufferDouble with explicit
authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull
read-only; restores/loads/resets push; a mid-batch handover drains the
device gamma/beta accumulator into the host arrays so no sample is lost.
3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at
init (one warning instead of warning + failed Execute). Build tag bumped to
win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five
binary-changing commits.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
//--- MEASURED GEOMETRY OVERRIDE (2026-08-09). When the AI signal has derived (or adopted from its
//--- .cfg) the barrier geometry its labels are built on, the LIVE trade uses that exact pair - both
//--- legs, all modes, including the Intelligent ones. Not optional and not blended with confidence,
//--- because the deploy gate's certificate is precise: "reaches g_DerivedTpAtrMult*ATR before
//--- g_DerivedSlAtrMult*ATR at a win rate above break-even". 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. Both-or-neither, same guard as every other consumer of a derived pair.
bool useDerivedGeometry = ( g_DerivedSlAtrMult > 0.0 & & g_DerivedTpAtrMult > 0.0 ) ;
2026-08-09 14:51:59 -04:00
double slMultiplier ;
fix: live trades now use the geometry the gate certifies; perf: BN kernels
Three changes, one theme: the trade placed, the trade graded, and the trade
computed are now the same trade.
1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier
pair reached the LABELS only - OpenParams still placed orders at the enum
geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before
3.33*ATR above break-even" about trades the EA never placed. Published via
g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick
contract as the confidence globals, because OpenParams runs on the root signal
which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at
era 0, and the .cfg adoption a deployed model takes. Overrides both legs and
both Intelligent modes - the certificate is exact or it is nothing. TP is
ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot
reshape the certified target.
2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl -
forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a
line-for-line transcription of the host implementation (NormalizeHost /
HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the
exact moment-write ordering. The host copies remain the runtime for the DLL
and pure-MQL5 tiers and the reference the kernels must match.
Because this box has no OpenCL platform, the safety story is layered:
- shim validation: kernels compiled as C and driven against a fp64 host
transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the
frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff
0.132 vs tolerance 1.0
- in-situ self-check: each kernel is compared against its host twin ON FIRST
USE on the real device (SelfCheckBn*), covering what the shim cannot - arg
indices and buffer bindings. Any disagreement resyncs from the good copy,
latches all BN kernels off process-wide, and training continues host-side.
A transcription bug costs a warning and some speed, never a poisoned .nnw.
- sync discipline: BatchOptions is now a CBufferDouble with explicit
authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull
read-only; restores/loads/resets push; a mid-batch handover drains the
device gamma/beta accumulator into the host arrays so no sample is lost.
3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at
init (one warning instead of warning + failed Execute). Build tag bumped to
win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five
binary-changing commits.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
if ( useDerivedGeometry )
slMultiplier = g_DerivedSlAtrMult ;
2026-08-09 14:51:59 -04:00
else
fix: live trades now use the geometry the gate certifies; perf: BN kernels
Three changes, one theme: the trade placed, the trade graded, and the trade
computed are now the same trade.
1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier
pair reached the LABELS only - OpenParams still placed orders at the enum
geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before
3.33*ATR above break-even" about trades the EA never placed. Published via
g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick
contract as the confidence globals, because OpenParams runs on the root signal
which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at
era 0, and the .cfg adoption a deployed model takes. Overrides both legs and
both Intelligent modes - the certificate is exact or it is nothing. TP is
ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot
reshape the certified target.
2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl -
forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a
line-for-line transcription of the host implementation (NormalizeHost /
HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the
exact moment-write ordering. The host copies remain the runtime for the DLL
and pure-MQL5 tiers and the reference the kernels must match.
Because this box has no OpenCL platform, the safety story is layered:
- shim validation: kernels compiled as C and driven against a fp64 host
transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the
frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff
0.132 vs tolerance 1.0
- in-situ self-check: each kernel is compared against its host twin ON FIRST
USE on the real device (SelfCheckBn*), covering what the shim cannot - arg
indices and buffer bindings. Any disagreement resyncs from the good copy,
latches all BN kernels off process-wide, and training continues host-side.
A transcription bug costs a warning and some speed, never a poisoned .nnw.
- sync discipline: BatchOptions is now a CBufferDouble with explicit
authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull
read-only; restores/loads/resets push; a mid-batch handover drains the
device gamma/beta accumulator into the host arrays so no sample is lost.
3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at
init (one warning instead of warning + failed Execute). Build tag bumped to
win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five
binary-changing commits.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
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 ;
2026-08-09 14:51:59 -04:00
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). Deliberately
// applied BEFORE take profit below: TP_INTELLIGENT sizes itself off the FINAL entry-to-stop distance,
// so a floor that widened the stop afterwards would silently shrink the realised reward:risk below the
// ratio that mode is meant to guarantee - and, at the shipped defaults, straight back under the Min RR
// rejection threshold.
if ( fabs ( price - sl ) < ( MIN_SL_ATR_MULTIPLIER * atr ) )
sl = isLong ? ( price - MIN_SL_ATR_MULTIPLIER * atr ) : ( price + MIN_SL_ATR_MULTIPLIER * atr ) ;
double risk = fabs ( price - sl ) ;
// --- Take profit: TP_ATR_* are an ATR multiple FROM THE ENTRY PRICE; TP_INTELLIGENT is a multiple of
// THIS TRADE'S OWN RISK, widening with confidence. Min RR (below) only rejects, never reshapes
// either. Now that the stop is entry-anchored, risk IS exactly slMultiplier*ATR, so the
// risk-relative and ATR-relative formulations coincide - TP_INTELLIGENT stays risk-relative
// because that keeps its reward:risk guarantee exact even after the MIN_SL_ATR_MULTIPLIER floor
// or TCAdjustStops() widens the stop (see TP_INTELLIGENT_BASE_RR's comment).
fix: live trades now use the geometry the gate certifies; perf: BN kernels
Three changes, one theme: the trade placed, the trade graded, and the trade
computed are now the same trade.
1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier
pair reached the LABELS only - OpenParams still placed orders at the enum
geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before
3.33*ATR above break-even" about trades the EA never placed. Published via
g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick
contract as the confidence globals, because OpenParams runs on the root signal
which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at
era 0, and the .cfg adoption a deployed model takes. Overrides both legs and
both Intelligent modes - the certificate is exact or it is nothing. TP is
ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot
reshape the certified target.
2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl -
forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a
line-for-line transcription of the host implementation (NormalizeHost /
HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the
exact moment-write ordering. The host copies remain the runtime for the DLL
and pure-MQL5 tiers and the reference the kernels must match.
Because this box has no OpenCL platform, the safety story is layered:
- shim validation: kernels compiled as C and driven against a fp64 host
transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the
frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff
0.132 vs tolerance 1.0
- in-situ self-check: each kernel is compared against its host twin ON FIRST
USE on the real device (SelfCheckBn*), covering what the shim cannot - arg
indices and buffer bindings. Any disagreement resyncs from the good copy,
latches all BN kernels off process-wide, and training continues host-side.
A transcription bug costs a warning and some speed, never a poisoned .nnw.
- sync discipline: BatchOptions is now a CBufferDouble with explicit
authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull
read-only; restores/loads/resets push; a mid-batch handover drains the
device gamma/beta accumulator into the host arrays so no sample is lost.
3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at
init (one warning instead of warning + failed Execute). Build tag bumped to
win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five
binary-changing commits.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
if ( useDerivedGeometry )
2026-08-09 14:51:59 -04:00
{
fix: live trades now use the geometry the gate certifies; perf: BN kernels
Three changes, one theme: the trade placed, the trade graded, and the trade
computed are now the same trade.
1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier
pair reached the LABELS only - OpenParams still placed orders at the enum
geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before
3.33*ATR above break-even" about trades the EA never placed. Published via
g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick
contract as the confidence globals, because OpenParams runs on the root signal
which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at
era 0, and the .cfg adoption a deployed model takes. Overrides both legs and
both Intelligent modes - the certificate is exact or it is nothing. TP is
ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot
reshape the certified target.
2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl -
forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a
line-for-line transcription of the host implementation (NormalizeHost /
HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the
exact moment-write ordering. The host copies remain the runtime for the DLL
and pure-MQL5 tiers and the reference the kernels must match.
Because this box has no OpenCL platform, the safety story is layered:
- shim validation: kernels compiled as C and driven against a fp64 host
transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the
frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff
0.132 vs tolerance 1.0
- in-situ self-check: each kernel is compared against its host twin ON FIRST
USE on the real device (SelfCheckBn*), covering what the shim cannot - arg
indices and buffer bindings. Any disagreement resyncs from the good copy,
latches all BN kernels off process-wide, and training continues host-side.
A transcription bug costs a warning and some speed, never a poisoned .nnw.
- sync discipline: BatchOptions is now a CBufferDouble with explicit
authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull
read-only; restores/loads/resets push; a mid-batch handover drains the
device gamma/beta accumulator into the host arrays so no sample is lost.
3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at
init (one warning instead of warning + failed Execute). Build tag bumped to
win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five
binary-changing commits.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
//--- 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 ) ;
2026-08-09 14:51:59 -04:00
}
else
fix: live trades now use the geometry the gate certifies; perf: BN kernels
Three changes, one theme: the trade placed, the trade graded, and the trade
computed are now the same trade.
1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier
pair reached the LABELS only - OpenParams still placed orders at the enum
geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before
3.33*ATR above break-even" about trades the EA never placed. Published via
g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick
contract as the confidence globals, because OpenParams runs on the root signal
which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at
era 0, and the .cfg adoption a deployed model takes. Overrides both legs and
both Intelligent modes - the certificate is exact or it is nothing. TP is
ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot
reshape the certified target.
2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl -
forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a
line-for-line transcription of the host implementation (NormalizeHost /
HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the
exact moment-write ordering. The host copies remain the runtime for the DLL
and pure-MQL5 tiers and the reference the kernels must match.
Because this box has no OpenCL platform, the safety story is layered:
- shim validation: kernels compiled as C and driven against a fp64 host
transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the
frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff
0.132 vs tolerance 1.0
- in-situ self-check: each kernel is compared against its host twin ON FIRST
USE on the real device (SelfCheckBn*), covering what the shim cannot - arg
indices and buffer bindings. Any disagreement resyncs from the good copy,
latches all BN kernels off process-wide, and training continues host-side.
A transcription bug costs a warning and some speed, never a poisoned .nnw.
- sync discipline: BatchOptions is now a CBufferDouble with explicit
authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull
read-only; restores/loads/resets push; a mid-batch handover drains the
device gamma/beta accumulator into the host arrays so no sample is lost.
3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at
init (one warning instead of warning + failed Execute). Build tag bumped to
win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five
binary-changing commits.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
if ( m_tp_mode = = TP_INTELLIGENT_MODE )
{
double targetRR = TP_INTELLIGENT_BASE_RR * ( 1.0 + AI_TP_WIDEN_FACTOR * confidence ) ;
tp = isLong ? m_symbol . NormalizePrice ( price + targetRR * risk )
: m_symbol . NormalizePrice ( price - targetRR * risk ) ;
}
else
{
double tpMultiplier = ( double ) m_tp_mode ;
tp = isLong ? m_symbol . NormalizePrice ( price + tpMultiplier * atr )
: m_symbol . NormalizePrice ( price - tpMultiplier * atr ) ;
}
2026-08-09 14:51:59 -04:00
// Guard rail: when both AI and classic share this path, any non-finite or negative level here is an
// upstream data/state issue, not a mode-specific feature. Reject early with full context.
if ( ! MathIsValidNumber ( price ) | | price < 0.0 | |
! MathIsValidNumber ( sl ) | | sl < 0.0 | |
! MathIsValidNumber ( tp ) | | tp < 0.0 )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " openparams-invalid-levels: " + m_symbol . Name ( ) ,
StringFormat ( " %s: rejected - invalid computed levels (isLong=%s, entryMode=%d, slMode=%d, tpMode=%d, atr=%g, base=%g, low=%g, high=%g, price=%g, sl=%g, tp=%g). " ,
__FUNCTION__ , isLong ? " true " : " false " , entryMode , m_sl_mode , m_tp_mode ,
atr , base_price , lowest_low , highest_high , price , sl , tp ) ) ;
return false ;
}
// --- Article 2555 #6: SL and TP must clear SYMBOL_TRADE_STOPS_LEVEL, measured against the price of
// the OPPOSITE operation for a market order (a long closes at Bid, a short at Ask) or against
// the activation price for a pending one. Nothing upstream enforced this: SL is anchored to a
// recent swing and TP to an ATR/RR multiple, both of which can land inside the broker's minimum
// distance on a quiet bar or a wide-spread symbol - the trade was then built, sized by Money,
// and rejected server-side with "Invalid stops" (10016) with nothing in the log explaining why.
// Which order type this becomes is decided by CExpertTrade::Buy()/Sell() purely from `price` vs
// ask/bid +- the stops level, so the same comparison is reproduced here to pick the type the
// stops will actually be validated against.
ENUM_ORDER_TYPE order_type = ResolveOrderType ( isLong , price ) ;
string stops_note ;
if ( ! TCAdjustStops ( m_symbol . Name ( ) , order_type , price , sl , tp , stops_note ) )
{
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. It could only
// ever veto a setup whose SL/TP the pipeline had already chosen, and vetoing on a ratio does not
// improve expectancy: it trades hit rate against payoff at a break-even the geometry already fixes.
// What it did do was reject 100% of setups on every symbol once, which is four Market validation
// failures for "no trading operations". Account risk % and CRiskBudget's drawdown enforcement are
// what bound risk here.
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 )
{
bool result = false ;
//--- check of exceeding the threshold value, adjusted for long/short
double directionMultiplier = isLong ? -1 : 1 ;
// Allowing position closing without checking the prohibition signal.
if ( directionMultiplier * m_direction > = m_threshold_close )
result = true ;
// AI-driven early exit: close regardless of the rule-based threshold above if the AI signal has flipped
// against the open position with at least m_ai_exit_threshold confidence. LiveSignedConfidence()
// supplies the AI signal's live value even on the non-AI aggregate/root signal this runs on, so this is
// a no-op when no AI signal is active/converged yet (it returns 0.0) or when Min_Vote_Close is Disabled
// (m_ai_exit_threshold resolves to 1.01, which no confidence magnitude can reach).
//
// This is NOT redundant with the averaged vote above, which is why it exists as a second route rather
// than being folded into it. The AI's ordinary vote is AVERAGED with every other filter's, so an AI
// reversal landing on a bar where that average stays under m_threshold_close is diluted away and the
// position stays open for as long as the dilution lasts. Reading the LIVE signed confidence here,
// undiluted and every bar, is what closes that hole. (This used to be a sharper problem: the vote was
// also one-shot, because the alternation gate was consumed on firing and never re-offered. That gate is
// gone as of 2026-08-01, so the remaining gap is dilution alone - still real, still worth this route.)
if ( ! result )
{
double signed_conf = LiveSignedConfidence ( ) ;
bool reversedAgainstLong = isLong & & signed_conf < 0.0 & & MathAbs ( signed_conf ) > = m_ai_exit_threshold ;
bool reversedAgainstShort = ! isLong & & signed_conf > 0.0 & & MathAbs ( signed_conf ) > = m_ai_exit_threshold ;
if ( reversedAgainstLong | | reversedAgainstShort )
result = true ;
}
if ( result )
{
//--- try to get the level of closing, differentiating based on isLong
if ( ! ( isLong ? CloseLongParams ( price ) : CloseShortParams ( price ) ) )
result = false ;
}
//--- zeroize the base price
m_base_price = 0.0 ;
//--- return the result
return result ;
}
//+------------------------------------------------------------------+
//| Generating a signal for closing of a long position |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : CheckCloseLong ( double & price )
{
return CheckClosePosition ( true , price ) ;
}
//+------------------------------------------------------------------+
//| Generating a signal for closing a short position |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : CheckCloseShort ( double & price )
{
return CheckClosePosition ( false , price ) ;
}
//+------------------------------------------------------------------+
//| Common function for opening positions |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : CheckOpenPosition ( bool isLong , double & price , double & sl , double & tp , datetime & expiration )
{
bool result = false ;
//--- the "prohibition" signal
if ( m_prohibition_signal = = true )
{
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " open-prohibition " ,
StringFormat ( " %s: open %s rejected - a child filter vetoed the tick (prohibition signal). " ,
__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 ;
//--- try to get the levels of opening, differentiating based on isLong
if ( ! ( isLong ? OpenLongParams ( price , sl , tp , expiration ) : OpenShortParams ( price , sl , tp , expiration ) ) )
{
// 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 ;
}
}
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
if ( tradingdirection = = LONG_ONLY | | tradingdirection = = BOTH )
{
return CheckOpenPosition ( true , price , sl , tp , expiration ) ;
}
// If the strategy is SHORT_ONLY, prevent opening a long position
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " open-long-direction-block " ,
StringFormat ( " %s: open long rejected - strategy direction blocks long entries. " , __FUNCTION__ ) ) ;
return false ;
}
//+------------------------------------------------------------------+
//| Generating a sell signal |
//+------------------------------------------------------------------+
bool CExpertSignalCustom : : CheckOpenShort ( double & price , double & sl , double & tp , datetime & expiration )
{
// Check if the trading strategy allows opening short positions
if ( tradingdirection = = SHORT_ONLY | | tradingdirection = = BOTH )
{
return CheckOpenPosition ( false , price , sl , tp , expiration ) ;
}
// If the strategy is LONG_ONLY, prevent opening a short position
if ( ShouldTraceTradeRejections ( ) )
TraceSignalRejection ( " open-short-direction-block " ,
StringFormat ( " %s: open short rejected - strategy direction blocks short entries. " , __FUNCTION__ ) ) ;
return false ;
}
//+------------------------------------------------------------------+
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//| Return the long ladder's matched pattern (consuming read) |
2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string CExpertSignalCustom : : GetActivePatternLong ( void )
2026-08-09 14:51:59 -04:00
{
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string ret = m_active_pattern_long ;
m_active_pattern_long = " NULL " ;
2026-08-09 14:51:59 -04:00
return ret ;
}
//+------------------------------------------------------------------+
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//| Return the short ladder's matched pattern (consuming read) |
2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string CExpertSignalCustom : : GetActivePatternShort ( void )
2026-08-09 14:51:59 -04:00
{
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string ret = m_active_pattern_short ;
m_active_pattern_short = " NULL " ;
2026-08-09 14:51:59 -04:00
return ret ;
}
//+------------------------------------------------------------------+
//| Detecting the "weighted" direction |
//+------------------------------------------------------------------+
double CExpertSignalCustom : : Direction ( void )
{
MqlDateTime gmtTime ;
datetime nowGMT = TimeGMT ( gmtTime ) ; // full timestamp AND broken-down form - both are used below
//--- Open a fresh intra-second averaging window whenever the second changes. This block may ONLY
//--- reset the window - it must never be the thing that publishes m_directionLastResult. It used to
//--- close the previous window here and return that value, which meant the value handed to
//--- CExpert(Custom)::SetDirection() -> m_direction (the field CheckOpenPosition/CheckClosePosition
//--- actually threshold against) was always the PREVIOUS second's average, never this call's own
//--- vote. With Expert_EveryTick=false, Direction() runs exactly once per bar at the bar open, so
//--- TimeGMT().sec is 0 on every single call: after the very first call the branch below never fired
//--- again, m_directionLastResult stayed pinned at its 0.0 seed forever, and m_direction was 0 on
//--- every bar - no signal could ever reach m_threshold_open and the EA could not open a single
//--- trade, in Classic, AI-only or Hybrid alike (they all inherit this one Direction() body). It also
//--- silently ate the AI vote entirely: at the time, CExpertSignalAIBase::LongCondition/ShortCondition
//--- consumed a one-shot alternation gate when they fired, so the discarded vote was never re-offered on
//--- a later bar (that gate was removed 2026-08-01; the ordering bug it amplified was real either way).
//--- The window average is now computed at the end of this function
//--- with this call's own result folded in, so what is returned always includes the current tick.
if ( nowGMT ! = m_directionCurrentSecond )
{
m_directionAggregatedResult = 0.0 ;
m_directionCount = 0 ;
m_directionCurrentSecond = nowGMT ; // 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()
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//--- 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 ;
double result = m_weight * ( longResult - shortResult ) ;
2026-08-09 14:51:59 -04:00
//--- Non-consuming quorum peek - see m_lastFiredDirection's declaration comment. Snapshotted from
//--- this filter's OWN vote, before the loop below adds any children's contributions in.
m_lastFiredDirection = ( result > 0.0 ) ? 1 : ( ( result < 0.0 ) ? -1 : 0 ) ;
int number = ( result = = 0.0 ) ? 0 : 1 ;
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. Quorum suppression
//--- (pass 2) needs every quorum-flagged filter's m_lastFiredDirection already fresh for THIS tick;
//--- checking mid-loop, as a single pass used to, would compare against filters not yet visited this
//--- iteration (stale, still holding last tick's value).
double directions [ ] ;
ArrayResize ( directions , total ) ;
bool aborted = false ;
for ( int i = 0 ; i < total ; i + + )
{
long mask = ( ( long ) 1 ) < < i ;
if ( ( m_ignore & mask ) ! = 0 )
{
directions [ i ] = EMPTY_VALUE ;
continue ;
}
CExpertSignalCustom * filter = m_filters . At ( i ) ;
if ( filter = = NULL )
{
Print ( " Error: Filter at index " + IntegerToString ( i ) + " is NULL " ) ;
directions [ i ] = EMPTY_VALUE ;
continue ;
}
string filterID = filter . GetFilterID ( ) ;
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//--- Per-side pattern journaling: each ladder that MATCHED on this filter's last evaluation
//--- writes its own row, labelled by its own side, with the filter's net vote stored as data
//--- (netVote column) rather than used as a drop filter. The previous design kept ONE
//--- last-writer-wins label across LongCondition() then ShortCondition() and only journaled it
//--- when it agreed with the net vote's sign. That gate was added to stop flat-vote bars from
//--- writing directional rows, but it censored structurally: a long event co-occurring with any
//--- short-side STATE model lost its label to the later writer and was dropped (vote positive,
//--- label "Sell"), while the mirrored short event journaled fine because the long ladder wrote
//--- first. Ichimoku models 0/3 and MA model 1 could not produce a row AT ALL by construction,
//--- and every pattern's recorded win rate was measured on a with-trend-only subset - the exact
//--- statistic UpdateSignalsWeights() feeds back into that pattern's weight, and a self-sealing
//--- loop: no rows -> no win rate -> default weight -> still censored. Per-side labels keep the
//--- flat-vote bug fixed without the censoring: a ladder that matched nothing has "NULL" and
//--- writes nothing, and a label can no longer contradict the side it is filed under. Like the
//--- single label before them, both slots (and LastNetVote()) are written by this filter's OWN
//--- Direction() and read here one tick later, so pattern and netVote describe the same tick.
2026-08-12 12:02:05 -04:00
//--- The log is unconditional on the DECISION layer: no OpenLongParams()/OpenShortParams() gate
//--- here any more. Those calls validate order placement (broker stops-level, ATR warm-up,
//--- entry-mode rejection), and their failures cluster in volatility/spread conditions - gating
//--- the log on them non-randomly censored exactly those bars out of every pattern's win-rate
//--- sample. The ledger doesn't need placement to be possible: its entries are marked at the
//--- touchable side of the spread below, and its exits are same-pattern reversals, not broker
//--- fills. Whether a tradable order could have been built from the signal is the decision
//--- layer's question, answered downstream from weights this log exists to inform.
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string patternLong = filter . GetActivePatternLong ( ) ;
string patternShort = filter . GetActivePatternShort ( ) ;
if ( filterID ! = " NULL " & & m_useDatabase )
2026-08-09 14:51:59 -04:00
{
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
double filterNetVote = filter . LastNetVote ( ) ;
2026-08-12 12:02:05 -04:00
if ( patternLong ! = " NULL " )
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
BufferNewTickSignal ( filterID , patternLong , " Buy " , gmtTime , m_symbol . Ask ( ) , filterNetVote ) ;
2026-08-12 12:02:05 -04:00
if ( patternShort ! = " NULL " )
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
BufferNewTickSignal ( filterID , patternShort , " Sell " , gmtTime , m_symbol . Bid ( ) , filterNetVote ) ;
2026-08-09 14:51:59 -04:00
}
double direction = filter . Direction ( ) ;
if ( direction = = EMPTY_VALUE )
{
m_prohibition_signal = true ;
directions [ i ] = EMPTY_VALUE ;
continue ;
}
// Validate the result to be within the range of -100 to 100
if ( direction < -100 | | direction > 100 )
{
PrintVerbose ( " A filter's direction is invalid. Skipping tick. " ) ;
result = 0 ;
number = 0 ;
aborted = true ;
break ;
}
directions [ i ] = direction ;
}
//--- The tick was discarded, so NO filter's vote was used - roll every one of them back, for the same
//--- reason a quorum-suppressed vote is rolled back in pass 2 below (see BeginVote()/RevokeVote()).
if ( aborted )
{
for ( int i = 0 ; i < total ; i + + )
{
CExpertSignalCustom * filter = m_filters . At ( i ) ;
if ( filter ! = NULL )
filter . RevokeVote ( ) ;
}
}
//--- Pass 2: sum each filter's cached contribution. Standard weighted voting only - no quorum gate.
if ( ! aborted )
{
for ( int i = 0 ; i < total ; i + + )
{
double direction = directions [ i ] ;
if ( direction = = EMPTY_VALUE | | direction = = 0 )
continue ;
CExpertSignalCustom * filter = m_filters . At ( i ) ;
number + + ; // Only increment `number` if `direction` is not 0 or EMPTY_VALUE and not suppressed
long mask = ( ( long ) 1 ) < < i ;
result + = ( ( m_invert & mask ) ! = 0 ) ? - direction : direction ;
}
}
//--- Normalization, as CExpertSignal::Direction() does it: the weighted votes are AVERAGED over the
//--- filters that actually voted, not summed. `number` was being counted here and then never used,
//--- which left result as a raw sum - two ordinary agreeing votes (e.g. MA's 60 + RSI's 100) could
//--- exceed the +-100 valid band and get zeroed by the range check below, throwing away exactly the
//--- strongest, most agreed-upon setups. Only non-zero, non-suppressed contributions increment
//--- `number` (see pass 2), so a lone filter voting 10 still normalizes to 10 and can clear a
//--- ThresholdOpen(10) on its own - averaging does not raise the bar for a single-voter signal.
if ( ! aborted & & number ! = 0 )
result / = number ;
//--- Fold this call's result into the current second's window and publish the window average - see
//--- the window-reset block at the top of this function for why this must happen here.
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. " ) ;
}
PrintVerbose ( " Final directional result: " + DoubleToString ( m_directionLastResult ) ) ;
return m_directionLastResult ;
}
//+------------------------------------------------------------------+
//| handles the new bar signal buffering |
//+------------------------------------------------------------------+
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
void CExpertSignalCustom : : BufferNewTickSignal ( string filterID , string pattern , string bias , const MqlDateTime & gmtTime , double entryPrice , double netVote )
2026-08-09 14:51:59 -04:00
{
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 ) ;
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
SignalInfo signal = { gmtTime . year , gmtTime . mon , gmtTime . day , gmtTime . day_of_week , gmtTime . hour , gmtTime . min , tableName , pattern , bias , entryPrice , netVote } ;
2026-08-09 14:51:59 -04:00
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 " ) ;
}
2026-08-12 18:53:04 -04:00
// Every question below is answered by a targeted SQL lookup returning one row or one number.
// The original design fetched BOTH full tables into MQL struct arrays per signal, which is the
// real constraint the historical 1000-row cap protected against: SQLite has no row limit, but
// materializing thousands of string-bearing structs per signal event does not scale, and an
// 18-year corpus build would have crawled. Per-signal cost is now flat in table size.
int curCount = 0 , oppCount = 0 ;
if ( ! dbm . FetchRecordCount ( currentTableName , curCount ) )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
Print ( " Failed to count current direction trades in: " + currentTableName ) ;
return ;
2026-08-09 14:51:59 -04:00
}
2026-08-12 18:53:04 -04:00
if ( ! dbm . FetchRecordCount ( oppositeTableName , oppCount ) )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
Print ( " Failed to count opposite direction trades in: " + oppositeTableName ) ;
return ;
2026-08-09 14:51:59 -04:00
}
2026-08-12 18:53:04 -04:00
if ( curCount > = m_maxTableRows )
2026-08-09 14:51:59 -04:00
DeleteOldestEntry ( currentTableName ) ;
2026-08-12 18:53:04 -04:00
if ( oppCount > = m_maxTableRows )
2026-08-09 14:51:59 -04:00
DeleteOldestEntry ( oppositeTableName ) ;
2026-08-12 18:53:04 -04:00
// Close the opposite direction's open trade, if any. Closing does NOT absorb the signal: the
2026-08-12 11:32:36 -04:00
// reversing signal still registers its own trade below (true stop-AND-reverse). It used to set a flag
// that skipped registration, which one-sided the ledger for every pure EVENT pattern: signals like
// MACD model 3 (zero-line cross) strictly alternate Buy/Sell, so each reversal was consumed as an
// exit and every row landed on whichever side fired first (measured: 60 Buy rows, 0 Sell rows over 7
// months). The side that never registered also never got a win rate, so UpdateSignalsWeights()
// weighted the pattern from one side only. State patterns escaped only by re-firing one bar later.
2026-08-12 18:53:04 -04:00
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 )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
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 ) ) ;
2026-08-09 14:51:59 -04:00
}
2026-08-12 18:53:04 -04:00
// 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. (This is also why a corpus-building backtest must
// start from an empty DB - see the warning in Expert\AIBase\MetaCorpus.mqh.)
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 )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
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 ;
2026-08-09 14:51:59 -04:00
}
2026-08-12 11:32:36 -04:00
// 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 ) ;
2026-08-09 14:51:59 -04:00
}
//+------------------------------------------------------------------+
2026-08-12 18:53:04 -04:00
//| 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 ;
}
//+------------------------------------------------------------------+
2026-08-09 14:51:59 -04:00
//| Helper function to compare two datetime values |
//+------------------------------------------------------------------+
bool IsEarlier ( const SignalInfo & a , const SignalInfo & b )
{
datetime dtA = MakeDateTime ( a ) ;
datetime dtB = MakeDateTime ( b ) ;
return dtA < dtB ;
}
//+------------------------------------------------------------------+
//| Selection sort for sorting SignalInfo array by datetime |
//+------------------------------------------------------------------+
void SelectionSort ( SignalInfo & signals [ ] , int size )
{
for ( int i = 0 ; i < size - 1 ; i + + )
{
int min_idx = i ;
for ( int j = i + 1 ; j < size ; j + + )
{
if ( IsEarlier ( signals [ j ] , signals [ min_idx ] ) )
{
min_idx = j ;
}
}
if ( min_idx ! = i )
{
// Swapping the elements
SignalInfo temp = signals [ i ] ;
signals [ i ] = signals [ min_idx ] ;
signals [ min_idx ] = temp ;
}
}
}
//+------------------------------------------------------------------+
//| Helper function to create a sortable datetime value |
//+------------------------------------------------------------------+
datetime MakeDateTime ( const SignalInfo & signal )
{
MqlDateTime t ;
t . year = signal . year ;
t . mon = signal . month ;
t . day = signal . day ;
t . hour = signal . hour ;
t . min = signal . minutes ;
t . sec = 0 ;
return StructToTime ( t ) ;
}
//+------------------------------------------------------------------+
//| Process the signal and update trades |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : ProcessBufferedSignals ( )
{
// Sort the signals array by datetime before processing
SelectionSort ( signalBuffer , ArraySize ( signalBuffer ) ) ;
if ( ! dbm . OpenDatabase ( ) )
{
Print ( " Failed to open database. " ) ;
return ;
}
if ( ! dbm . BeginTransaction ( ) )
{
Print ( __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 |
//+------------------------------------------------------------------+
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
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 )
2026-08-09 14:51:59 -04:00
{
fix(db): per-side pattern journaling + versioned journaling semantics
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
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 ) } ;
2026-08-09 14:51:59 -04:00
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 ;
2026-08-12 18:53:04 -04:00
//--- 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 gmtNow ;
TimeGMT ( gmtNow ) ;
long nowKey = SignalTimeKey ( gmtNow . year , gmtNow . mon , gmtNow . day , gmtNow . hour , gmtNow . min ) ;
2026-08-09 14:51:59 -04:00
for ( int i = 0 ; i < total ; i + + )
{
CExpertSignalCustom * filter = m_filters . At ( i ) ;
//--- check pointer
if ( filter = = NULL )
continue ;
string filterID = filter . GetFilterID ( ) ;
if ( filterID = = " NULL " )
continue ;
int patternCount = filter . GetPatternCount ( ) ;
if ( patternCount < = 0 | | patternCount = = NULL )
continue ;
int totalWinRate = 0 ;
int validPatternCount = 0 ;
for ( int j = 0 ; j < patternCount ; j + + )
{
2026-08-12 18:53:04 -04:00
// 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).
2026-08-09 14:51:59 -04:00
string pattern = PatternName ( j ) ;
string tableNameBuy = PatternTableName ( filterID , pattern , " Buy " ) ;
string tableNameSell = PatternTableName ( filterID , pattern , " Sell " ) ;
2026-08-12 18:53:04 -04:00
int winsBuy = 0 , lossesBuy = 0 , winsSell = 0 , lossesSell = 0 ;
if ( ! dbm . FetchWinLossCounts ( tableNameBuy , nowKey , winsBuy , lossesBuy ) )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
Print ( __FUNCTION__ + " Failed to count outcomes in " + tableNameBuy ) ;
2026-08-09 14:51:59 -04:00
continue ;
}
2026-08-12 18:53:04 -04:00
if ( ! dbm . FetchWinLossCounts ( tableNameSell , nowKey , winsSell , lossesSell ) )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
Print ( __FUNCTION__ + " Failed to count outcomes in " + tableNameSell ) ;
2026-08-09 14:51:59 -04:00
continue ;
}
2026-08-12 18:53:04 -04:00
int winRateBuy = WinRateFromCounts ( winsBuy , lossesBuy ) ;
int winRateSell = WinRateFromCounts ( winsSell , lossesSell ) ;
2026-08-09 14:51:59 -04:00
// Skip sides with insufficient samples instead of averaging in the sentinel
if ( winRateBuy = = NO_DATA_WIN_RATE & & winRateSell = = NO_DATA_WIN_RATE )
continue ;
int combinedWinRate = ( winRateBuy = = NO_DATA_WIN_RATE ) ? winRateSell :
( winRateSell = = NO_DATA_WIN_RATE ) ? winRateBuy :
( winRateBuy + winRateSell ) / 2 ;
if ( combinedWinRate > = 0 & & combinedWinRate < = 100 )
{
filter . ApplyPatternWeight ( j , combinedWinRate ) ;
totalWinRate + = combinedWinRate ;
validPatternCount + + ;
PrintVerbose ( " Applied " + filterID + " " + pattern + " Weight " + IntegerToString ( combinedWinRate ) ) ;
}
}
// Calculate the average win rate for valid patterns
double averageWinRate = validPatternCount > 0 ? ( totalWinRate ) / validPatternCount : 0.0 ;
// Normalize the average win rate to the range 0 to 1
double normalizedWinRate = averageWinRate / 100.0 ;
// Round the normalized win rate to the nearest 0.05
normalizedWinRate = MathRound ( normalizedWinRate * 10 ) / 10.0 ;
// Ensure the rounded value is within 0 to 1
normalizedWinRate = MathMax ( 0 , MathMin ( normalizedWinRate , 1 ) ) ;
// Apply the main weight based on the normalized and rounded win rate
double moduleWeight = normalizedWinRate ;
if ( moduleWeight > 0 & & moduleWeight < = 1 )
{
filter . Weight ( moduleWeight ) ;
PrintVerbose ( " Applied " + filterID + " Main Weight " + DoubleToString ( moduleWeight , 2 ) ) ;
}
if ( validPatternCount > 0 )
{
sumModuleWeight + = normalizedWinRate ;
weightedFilterCount + + ;
}
}
// Track the overall DB win-rate confidence across all filters, so it can be
// combined with (or used instead of) AI confidence via Confidence_Source.
m_dbConfidence = weightedFilterCount > 0 ? sumModuleWeight / weightedFilterCount : 0.0 ;
if ( dbm . CommitTransaction ( ) )
return true ;
else
return ( false ) ;
}
//+------------------------------------------------------------------+
2026-08-12 18:53:04 -04:00
//| Win rate from SQL-side outcome counts (see FetchWinLossCounts) |
2026-08-09 14:51:59 -04:00
//+------------------------------------------------------------------+
2026-08-12 18:53:04 -04:00
int CExpertSignalCustom : : WinRateFromCounts ( const int wins , const int losses )
2026-08-09 14:51:59 -04:00
{
2026-08-12 18:53:04 -04:00
int totalTrades = wins + losses ;
2026-08-09 14:51:59 -04:00
if ( totalTrades < MIN_TRADES_FOR_WIN_RATE )
return NO_DATA_WIN_RATE ;
2026-08-12 18:53:04 -04:00
return NormalizeWinRate ( 100.0 * wins / totalTrades ) ;
2026-08-09 14:51:59 -04:00
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
int CExpertSignalCustom : : NormalizeWinRate ( double winRate )
{
return ( int ) MathRound ( winRate / 10 ) * 10 ; // Round to the nearest 10
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : OnTickHandler ( void )
{
int total = m_filters . Total ( ) ;
for ( int i = 0 ; i < total ; i + + )
{
CExpertSignalCustom * filter = m_filters . At ( i ) ;
//--- check pointer
if ( filter = = NULL )
continue ;
string filterID = filter . GetFilterID ( ) ;
if ( filterID = = " NULL " )
continue ;
filter . OnTickHandler ( ) ;
}
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CExpertSignalCustom : : OnChartEventHandler ( const int id ,
const long & lparam ,
const double & dparam ,
const string & sparam )
{
int total = m_filters . Total ( ) ;
for ( int i = 0 ; i < total ; i + + )
{
CExpertSignalCustom * filter = m_filters . At ( i ) ;
//--- check pointer
if ( filter = = NULL )
continue ;
string filterID = filter . GetFilterID ( ) ;
if ( filterID = = " NULL " )
continue ;
filter . OnChartEventHandler ( id , lparam , dparam , sparam ) ;
}
}
//+------------------------------------------------------------------+